refactor: serverless module
This commit is contained in:
@@ -110,6 +110,7 @@ docker-compose up -d
|
||||
│ ├── say
|
||||
│ ├── search
|
||||
| ├── snippet
|
||||
| ├── serverless
|
||||
│ ├── sitemap
|
||||
│ ├── tool
|
||||
│ └── user
|
||||
|
||||
@@ -33,6 +33,7 @@ import { PTYModule } from './modules/pty/pty.module'
|
||||
import { RecentlyModule } from './modules/recently/recently.module'
|
||||
import { SayModule } from './modules/say/say.module'
|
||||
import { SearchModule } from './modules/search/search.module'
|
||||
import { ServerlessModule } from './modules/serverless/serverless.module'
|
||||
import { SitemapModule } from './modules/sitemap/sitemap.module'
|
||||
import { SnippetModule } from './modules/snippet/snippet.module'
|
||||
import { ToolModule } from './modules/tool/tool.module'
|
||||
@@ -70,6 +71,7 @@ import { LoggerModule } from './processors/logger/logger.module'
|
||||
RecentlyModule,
|
||||
SayModule,
|
||||
SearchModule,
|
||||
ServerlessModule,
|
||||
SitemapModule,
|
||||
SnippetModule,
|
||||
ToolModule,
|
||||
|
||||
@@ -12,9 +12,9 @@ import { AdminEventsGateway } from '~/processors/gateway/admin/events.gateway'
|
||||
import { EventTypes } from '~/processors/gateway/events.types'
|
||||
import { WebEventsGateway } from '~/processors/gateway/web/events.gateway'
|
||||
import { PagerDto } from '~/shared/dto/pager.dto'
|
||||
import { createMockedContextResponse } from '../snippet/mock-response.util'
|
||||
import { createMockedContextResponse } from '../serverless/mock-response.util'
|
||||
import { ServerlessService } from '../serverless/serverless.service'
|
||||
import { SnippetModel, SnippetType } from '../snippet/snippet.model'
|
||||
import { SnippetService } from '../snippet/snippet.service'
|
||||
|
||||
@Controller('debug')
|
||||
export class DebugController {
|
||||
@@ -22,7 +22,7 @@ export class DebugController {
|
||||
private readonly webEvent: WebEventsGateway,
|
||||
private readonly adminEvent: AdminEventsGateway,
|
||||
|
||||
private readonly snippetService: SnippetService,
|
||||
private readonly serverlessService: ServerlessService,
|
||||
) {}
|
||||
@Get('qs')
|
||||
async qs(@Query() query: PagerDto) {
|
||||
@@ -58,7 +58,7 @@ export class DebugController {
|
||||
model.private = false
|
||||
model.type = SnippetType.Function
|
||||
NotFoundException
|
||||
return await this.snippetService.injectContextIntoServerlessFunctionAndCall(
|
||||
return await this.serverlessService.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req, res: createMockedContextResponse() },
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { SnippetModule } from '../snippet/snippet.module'
|
||||
import { ServerlessModule } from '../serverless/serverless.module'
|
||||
import { DebugController } from './debug.controller'
|
||||
|
||||
@Module({
|
||||
controllers: [DebugController],
|
||||
imports: [SnippetModule],
|
||||
imports: [ServerlessModule],
|
||||
})
|
||||
export class DebugModule {}
|
||||
|
||||
48
src/modules/serverless/serverless.controller.ts
Normal file
48
src/modules/serverless/serverless.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Request,
|
||||
} from '@nestjs/common'
|
||||
import type { FastifyRequest } from 'fastify'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { IsMaster } from '~/common/decorator/role.decorator'
|
||||
import { createMockedContextResponse } from './mock-response.util'
|
||||
import { ServerlessReferenceDto } from './serverless.dto'
|
||||
import { ServerlessService } from './serverless.service'
|
||||
|
||||
@ApiName
|
||||
@Controller('serverless')
|
||||
export class ServerlessController {
|
||||
constructor(private readonly serverlessService: ServerlessService) {}
|
||||
|
||||
@Get('/:reference/:name')
|
||||
@HTTPDecorators.Bypass
|
||||
async runServerlessFunction(
|
||||
@Param() param: ServerlessReferenceDto,
|
||||
@IsMaster() isMaster: boolean,
|
||||
|
||||
@Request() req: FastifyRequest,
|
||||
) {
|
||||
const { name, reference } = param
|
||||
const snippet = await this.serverlessService.model.findOne({
|
||||
name,
|
||||
reference,
|
||||
})
|
||||
|
||||
if (!snippet) {
|
||||
throw new NotFoundException('snippet is not found')
|
||||
}
|
||||
|
||||
if (snippet.private && !isMaster) {
|
||||
throw new ForbiddenException('no permission to run this function')
|
||||
}
|
||||
return this.serverlessService.injectContextIntoServerlessFunctionAndCall(
|
||||
snippet,
|
||||
{ req, res: createMockedContextResponse() },
|
||||
)
|
||||
}
|
||||
}
|
||||
11
src/modules/serverless/serverless.dto.ts
Normal file
11
src/modules/serverless/serverless.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator'
|
||||
|
||||
export class ServerlessReferenceDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
reference: string
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string
|
||||
}
|
||||
10
src/modules/serverless/serverless.module.ts
Normal file
10
src/modules/serverless/serverless.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { ServerlessController } from './serverless.controller'
|
||||
import { ServerlessService } from './serverless.service'
|
||||
|
||||
@Module({
|
||||
controllers: [ServerlessController],
|
||||
providers: [ServerlessService],
|
||||
exports: [ServerlessService],
|
||||
})
|
||||
export class ServerlessModule {}
|
||||
@@ -151,4 +151,5 @@ And other global api is all banned.
|
||||
- [ ] handle safeEval throw
|
||||
- [ ] MongoDb inject (can access db)
|
||||
- [ ] set Content-Type
|
||||
- [ ] ESM AST Parser
|
||||
- [ ] ESM AST Parser
|
||||
- [ ] Cron to clean require cache
|
||||
241
src/modules/serverless/serverless.service.ts
Normal file
241
src/modules/serverless/serverless.service.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { isURL } from 'class-validator'
|
||||
import fs, { mkdir, stat } from 'fs/promises'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { join } from 'path'
|
||||
import { nextTick } from 'process'
|
||||
import { DATA_DIR, NODE_REQUIRE_PATH } from '~/constants/path.constant'
|
||||
import { AssetService } from '~/processors/helper/helper.asset.service'
|
||||
import { HttpService } from '~/processors/helper/helper.http.service'
|
||||
import { UniqueArray } from '~/ts-hepler/unique'
|
||||
import { safePathJoin } from '~/utils'
|
||||
import { safeEval } from '~/utils/safe-eval.util'
|
||||
import { isBuiltinModule } from '~/utils/sys.util'
|
||||
import type PKG from '../../../package.json'
|
||||
import { SnippetModel } from '../snippet/snippet.model'
|
||||
import {
|
||||
FunctionContextRequest,
|
||||
FunctionContextResponse,
|
||||
} from './function.types'
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessService {
|
||||
constructor(
|
||||
@InjectModel(SnippetModel)
|
||||
private readonly snippetModel: MongooseModel<SnippetModel>,
|
||||
private readonly assetService: AssetService,
|
||||
private readonly httpService: HttpService,
|
||||
) {
|
||||
nextTick(() => {
|
||||
// Add /includes/plugin to the path, also note that we need to support
|
||||
// `require('../hello.js')`. We can do that by adding /includes/plugin/a,
|
||||
// /includes/plugin/a/b, etc.. to the list
|
||||
mkdir(NODE_REQUIRE_PATH, { recursive: true }).then(async () => {
|
||||
const pkgPath = join(NODE_REQUIRE_PATH, 'package.json')
|
||||
|
||||
const isPackageFileExist = await stat(pkgPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (!isPackageFileExist) {
|
||||
await fs.writeFile(
|
||||
pkgPath,
|
||||
JSON.stringify({ name: 'modules' }, null, 2),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
module.paths.push(NODE_REQUIRE_PATH)
|
||||
|
||||
// if (isDev) {
|
||||
// console.log(module.paths)
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
public get model() {
|
||||
return this.snippetModel
|
||||
}
|
||||
async injectContextIntoServerlessFunctionAndCall(
|
||||
model: SnippetModel,
|
||||
context: { req: FunctionContextRequest; res: FunctionContextResponse },
|
||||
) {
|
||||
const { raw: functionString } = model
|
||||
const logger = new Logger('ServerlessFunction/' + model.name)
|
||||
const document = await this.model.findById(model.id)
|
||||
const globalContext = {
|
||||
context: {
|
||||
// inject app req, res
|
||||
...context,
|
||||
...context.res,
|
||||
query: context.req.query,
|
||||
headers: context.req.headers,
|
||||
params: context.req.params,
|
||||
|
||||
model,
|
||||
document,
|
||||
name: model.name,
|
||||
reference: model.reference,
|
||||
|
||||
// TODO
|
||||
// write file to asset
|
||||
writeAsset: async (
|
||||
path: string,
|
||||
data: any,
|
||||
options: Parameters<typeof fs.writeFile>[2],
|
||||
) => {
|
||||
return await this.assetService.writeUserCustomAsset(
|
||||
safePathJoin(path),
|
||||
data,
|
||||
options,
|
||||
)
|
||||
},
|
||||
// read file to asset
|
||||
readAsset: async (
|
||||
path: string,
|
||||
options: Parameters<typeof fs.readFile>[1],
|
||||
) => {
|
||||
return await this.assetService.getAsset(safePathJoin(path), options)
|
||||
},
|
||||
},
|
||||
// inject global
|
||||
__dirname: DATA_DIR,
|
||||
|
||||
// inject some zx utils
|
||||
fetch,
|
||||
|
||||
// inject Global API
|
||||
Buffer,
|
||||
|
||||
// inject logger
|
||||
console: logger,
|
||||
logger,
|
||||
|
||||
require: (() => {
|
||||
const __require = (id: string) => {
|
||||
const module = require(id)
|
||||
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
const __requireNoCache = (id: string) => {
|
||||
delete require.cache[require.resolve(id)]
|
||||
const module = require(id)
|
||||
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
async function $require(
|
||||
this: ServerlessService,
|
||||
id: string,
|
||||
useCache = true,
|
||||
) {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw new Error('require id is not valid')
|
||||
}
|
||||
|
||||
// 1. if is remote module
|
||||
if (
|
||||
isURL(id, { protocols: ['http', 'https'], require_protocol: true })
|
||||
) {
|
||||
const text = await this.httpService.getAndCacheRequest(id)
|
||||
return await safeEval(`${text}; return module.exports`, {
|
||||
exports: {},
|
||||
module: {
|
||||
exports: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 2. if application third part lib
|
||||
|
||||
const allowedThirdPartLibs: UniqueArray<
|
||||
(keyof typeof PKG.dependencies)[]
|
||||
> = [
|
||||
'algoliasearch',
|
||||
'axios-retry',
|
||||
'axios',
|
||||
'class-transformer',
|
||||
'class-validator',
|
||||
'dayjs',
|
||||
'ejs',
|
||||
'html-minifier',
|
||||
'image-size',
|
||||
'isbot',
|
||||
'js-yaml',
|
||||
'jsdom',
|
||||
'jszip',
|
||||
'lodash',
|
||||
'marked',
|
||||
'nanoid',
|
||||
'qs',
|
||||
'rxjs',
|
||||
'snakecase-keys',
|
||||
'ua-parser-js',
|
||||
'xss',
|
||||
]
|
||||
|
||||
const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-']
|
||||
|
||||
if (
|
||||
allowedThirdPartLibs.includes(id as any) ||
|
||||
trustPackagePrefixes.some((prefix) => id.startsWith(prefix))
|
||||
) {
|
||||
return useCache ? __require(id) : __requireNoCache(id)
|
||||
}
|
||||
|
||||
// 3. mock built-in module
|
||||
|
||||
const mockModules = {
|
||||
fs: {
|
||||
writeFile: globalContext.context.writeAsset,
|
||||
readFile: globalContext.context.readAsset,
|
||||
},
|
||||
}
|
||||
|
||||
if (Object.keys(mockModules).includes(id)) {
|
||||
return mockModules[id]
|
||||
}
|
||||
|
||||
// fin. is built-in module
|
||||
const module = isBuiltinModule(id, [
|
||||
'fs',
|
||||
'os',
|
||||
'child_process',
|
||||
'sys',
|
||||
])
|
||||
if (!module) {
|
||||
throw new Error(`cannot require ${id}`)
|
||||
} else {
|
||||
return __require(id)
|
||||
}
|
||||
}
|
||||
|
||||
return $require.bind(this)
|
||||
})(),
|
||||
process: {
|
||||
env: Object.freeze({ ...process.env }),
|
||||
nextTick: process.nextTick,
|
||||
},
|
||||
}
|
||||
|
||||
return await safeEval(
|
||||
`${functionString}; return handler(context, require)`,
|
||||
{ ...globalContext, global: globalContext, globalThis: globalContext },
|
||||
)
|
||||
}
|
||||
|
||||
async isValidServerlessFunction(raw: string) {
|
||||
try {
|
||||
return safeEval(`
|
||||
${raw}
|
||||
// 验证 handler 是否存在并且是函数
|
||||
return typeof handler === 'function'
|
||||
`)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
@@ -8,9 +9,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
} from '@nestjs/common'
|
||||
import type { FastifyRequest } from 'fastify'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { HttpCache } from '~/common/decorator/cache.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
@@ -19,7 +18,6 @@ import { IsMaster } from '~/common/decorator/role.decorator'
|
||||
import { MongoIdDto } from '~/shared/dto/id.dto'
|
||||
import { PagerDto } from '~/shared/dto/pager.dto'
|
||||
import { transformDataToPaginate } from '~/utils/transfrom.util'
|
||||
import { createMockedContextResponse } from './mock-response.util'
|
||||
import { SnippetModel, SnippetType } from './snippet.model'
|
||||
import { SnippetService } from './snippet.service'
|
||||
|
||||
@@ -68,8 +66,6 @@ export class SnippetController {
|
||||
@Param('name') name: string,
|
||||
@Param('reference') reference: string,
|
||||
@IsMaster() isMaster: boolean,
|
||||
|
||||
@Request() req: FastifyRequest,
|
||||
) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new ForbiddenException('name should be string')
|
||||
@@ -87,11 +83,12 @@ export class SnippetController {
|
||||
if (snippet.type !== SnippetType.Function) {
|
||||
return this.snippetService.attachSnippet(snippet).then((res) => res.data)
|
||||
}
|
||||
// run serverless function
|
||||
return this.snippetService.injectContextIntoServerlessFunctionAndCall(
|
||||
snippet,
|
||||
{ req, res: createMockedContextResponse() },
|
||||
)
|
||||
|
||||
if (snippet.type === SnippetType.Function) {
|
||||
throw new BadRequestException(
|
||||
'this snippet should run in serverless function scope.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
|
||||
@@ -49,7 +49,7 @@ export class SnippetModel extends BaseModel {
|
||||
|
||||
@prop({ require: true, trim: true })
|
||||
@IsNotEmpty()
|
||||
@Matches(/^[a-zA-Z0-9_]{1,30}$/, {
|
||||
@Matches(/^[a-zA-Z0-9_-]{1,30}$/, {
|
||||
message: 'name 只能使用英文字母和数字下划线且不超过 30 个字符',
|
||||
})
|
||||
name: string
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 数据配置区块
|
||||
*/
|
||||
import { Module } from '@nestjs/common'
|
||||
import { forwardRef, Module } from '@nestjs/common'
|
||||
import { ServerlessModule } from '../serverless/serverless.module'
|
||||
import { SnippetController } from './snippet.controller'
|
||||
import { SnippetService } from './snippet.service'
|
||||
|
||||
@@ -9,5 +10,6 @@ import { SnippetService } from './snippet.service'
|
||||
controllers: [SnippetController],
|
||||
exports: [SnippetService],
|
||||
providers: [SnippetService],
|
||||
imports: [forwardRef(() => ServerlessModule)],
|
||||
})
|
||||
export class SnippetModule {}
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common'
|
||||
import { isURL } from 'class-validator'
|
||||
import fs, { mkdir, stat } from 'fs/promises'
|
||||
import { load } from 'js-yaml'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { join } from 'path'
|
||||
import { nextTick } from 'process'
|
||||
import type PKG from '~/../package.json'
|
||||
import { DATA_DIR, NODE_REQUIRE_PATH } from '~/constants/path.constant'
|
||||
import { AssetService } from '~/processors/helper/helper.asset.service'
|
||||
import { HttpService } from '~/processors/helper/helper.http.service'
|
||||
import { UniqueArray } from '~/ts-hepler/unique'
|
||||
import { safePathJoin } from '~/utils'
|
||||
import { safeEval } from '~/utils/safe-eval.util'
|
||||
import { isBuiltinModule } from '~/utils/sys.util'
|
||||
import {
|
||||
FunctionContextRequest,
|
||||
FunctionContextResponse,
|
||||
} from './function.types'
|
||||
import { ServerlessService } from '../serverless/serverless.service'
|
||||
import { SnippetModel, SnippetType } from './snippet.model'
|
||||
|
||||
@Injectable()
|
||||
@@ -30,34 +15,9 @@ export class SnippetService {
|
||||
constructor(
|
||||
@InjectModel(SnippetModel)
|
||||
private readonly snippetModel: MongooseModel<SnippetModel>,
|
||||
private readonly assetService: AssetService,
|
||||
private readonly httpService: HttpService,
|
||||
) {
|
||||
nextTick(() => {
|
||||
// Add /includes/plugin to the path, also note that we need to support
|
||||
// `require('../hello.js')`. We can do that by adding /includes/plugin/a,
|
||||
// /includes/plugin/a/b, etc.. to the list
|
||||
mkdir(NODE_REQUIRE_PATH, { recursive: true }).then(async () => {
|
||||
const pkgPath = join(NODE_REQUIRE_PATH, 'package.json')
|
||||
const isPackageFileExist = await stat(pkgPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (!isPackageFileExist) {
|
||||
await fs.writeFile(
|
||||
pkgPath,
|
||||
JSON.stringify({ name: 'modules' }, null, 2),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
module.paths.push(NODE_REQUIRE_PATH)
|
||||
|
||||
// if (isDev) {
|
||||
// console.log(module.paths)
|
||||
// }
|
||||
})
|
||||
}
|
||||
@Inject(forwardRef(() => ServerlessService))
|
||||
private readonly serverlessService: ServerlessService,
|
||||
) {}
|
||||
|
||||
get model() {
|
||||
return this.snippetModel
|
||||
@@ -107,7 +67,9 @@ export class SnippetService {
|
||||
break
|
||||
}
|
||||
case SnippetType.Function: {
|
||||
const isValid = await this.isValidServerlessFunction(model.raw)
|
||||
const isValid = await this.serverlessService.isValidServerlessFunction(
|
||||
model.raw,
|
||||
)
|
||||
if (!isValid) {
|
||||
throw new BadRequestException('serverless function is not valid')
|
||||
}
|
||||
@@ -121,189 +83,6 @@ export class SnippetService {
|
||||
}
|
||||
}
|
||||
|
||||
async injectContextIntoServerlessFunctionAndCall(
|
||||
model: SnippetModel,
|
||||
context: { req: FunctionContextRequest; res: FunctionContextResponse },
|
||||
) {
|
||||
const { raw: functionString } = model
|
||||
const logger = new Logger('ServerlessFunction/' + model.name)
|
||||
const document = await this.model.findById(model.id)
|
||||
const globalContext = {
|
||||
context: {
|
||||
// inject app req, res
|
||||
...context,
|
||||
...context.res,
|
||||
query: context.req.query,
|
||||
headers: context.req.headers,
|
||||
params: context.req.params,
|
||||
|
||||
model,
|
||||
document,
|
||||
name: model.name,
|
||||
reference: model.reference,
|
||||
|
||||
// TODO
|
||||
// write file to asset
|
||||
writeAsset: async (
|
||||
path: string,
|
||||
data: any,
|
||||
options: Parameters<typeof fs.writeFile>[2],
|
||||
) => {
|
||||
return await this.assetService.writeUserCustomAsset(
|
||||
safePathJoin(path),
|
||||
data,
|
||||
options,
|
||||
)
|
||||
},
|
||||
// read file to asset
|
||||
readAsset: async (
|
||||
path: string,
|
||||
options: Parameters<typeof fs.readFile>[1],
|
||||
) => {
|
||||
return await this.assetService.getAsset(safePathJoin(path), options)
|
||||
},
|
||||
},
|
||||
// inject global
|
||||
__dirname: DATA_DIR,
|
||||
|
||||
// inject some zx utils
|
||||
fetch,
|
||||
|
||||
// inject Global API
|
||||
Buffer,
|
||||
|
||||
// inject logger
|
||||
console: logger,
|
||||
logger,
|
||||
|
||||
require: (() => {
|
||||
const __require = (id: string) => {
|
||||
const module = require(id)
|
||||
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
const __requireNoCache = (id: string) => {
|
||||
delete require.cache[require.resolve(id)]
|
||||
const module = require(id)
|
||||
delete require.cache[require.resolve(id)]
|
||||
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
async function $require(
|
||||
this: SnippetService,
|
||||
id: string,
|
||||
useCache = true,
|
||||
) {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw new Error('require id is not valid')
|
||||
}
|
||||
|
||||
// 1. if is remote module
|
||||
if (
|
||||
isURL(id, { protocols: ['http', 'https'], require_protocol: true })
|
||||
) {
|
||||
const text = await this.httpService.getAndCacheRequest(id)
|
||||
return await safeEval(`${text}; return module.exports`, {
|
||||
exports: {},
|
||||
module: {
|
||||
exports: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 2. if application third part lib
|
||||
|
||||
const allowedThirdPartLibs: UniqueArray<
|
||||
(keyof typeof PKG.dependencies)[]
|
||||
> = [
|
||||
'algoliasearch',
|
||||
'axios-retry',
|
||||
'axios',
|
||||
'class-transformer',
|
||||
'class-validator',
|
||||
'dayjs',
|
||||
'ejs',
|
||||
'html-minifier',
|
||||
'image-size',
|
||||
'isbot',
|
||||
'js-yaml',
|
||||
'jsdom',
|
||||
'jszip',
|
||||
'lodash',
|
||||
'marked',
|
||||
'nanoid',
|
||||
'qs',
|
||||
'rxjs',
|
||||
'snakecase-keys',
|
||||
'ua-parser-js',
|
||||
'xss',
|
||||
]
|
||||
|
||||
const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-']
|
||||
|
||||
if (
|
||||
allowedThirdPartLibs.includes(id as any) ||
|
||||
trustPackagePrefixes.some((prefix) => id.startsWith(prefix))
|
||||
) {
|
||||
return useCache ? __require(id) : __requireNoCache(id)
|
||||
}
|
||||
|
||||
// 3. mock built-in module
|
||||
|
||||
const mockModules = {
|
||||
fs: {
|
||||
writeFile: globalContext.context.writeAsset,
|
||||
readFile: globalContext.context.readAsset,
|
||||
},
|
||||
}
|
||||
|
||||
if (Object.keys(mockModules).includes(id)) {
|
||||
return mockModules[id]
|
||||
}
|
||||
|
||||
// fin. is built-in module
|
||||
const module = isBuiltinModule(id, [
|
||||
'fs',
|
||||
'os',
|
||||
'child_process',
|
||||
'sys',
|
||||
])
|
||||
if (!module) {
|
||||
throw new Error(`cannot require ${id}`)
|
||||
} else {
|
||||
return __require(id)
|
||||
}
|
||||
}
|
||||
|
||||
return $require.bind(this)
|
||||
})(),
|
||||
process: {
|
||||
env: Object.freeze({ ...process.env }),
|
||||
nextTick: process.nextTick,
|
||||
},
|
||||
}
|
||||
|
||||
return await safeEval(
|
||||
`${functionString}; return handler(context, require)`,
|
||||
{ ...globalContext, global: globalContext, globalThis: globalContext },
|
||||
)
|
||||
}
|
||||
|
||||
async isValidServerlessFunction(raw: string) {
|
||||
try {
|
||||
return safeEval(`
|
||||
${raw}
|
||||
// 验证 handler 是否存在并且是函数
|
||||
return typeof handler === 'function'
|
||||
`)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getSnippetById(id: string) {
|
||||
const doc = await this.model.findById(id).lean()
|
||||
if (!doc) {
|
||||
|
||||
136
test/src/modules/serverless/serverless.service.spec.ts
Normal file
136
test/src/modules/serverless/serverless.service.spec.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Test } from '@nestjs/testing'
|
||||
import { getModelForClass } from '@typegoose/typegoose'
|
||||
import { getModelToken } from 'nestjs-typegoose'
|
||||
import { dbHelper } from 'test/helper/db-mock.helper'
|
||||
import { createMockedContextResponse } from '~/modules/serverless/mock-response.util'
|
||||
import { ServerlessService } from '~/modules/serverless/serverless.service'
|
||||
import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model'
|
||||
import { CacheService } from '~/processors/cache/cache.service'
|
||||
import { AssetService } from '~/processors/helper/helper.asset.service'
|
||||
import { HttpService } from '~/processors/helper/helper.http.service'
|
||||
|
||||
describe('test serverless function service', () => {
|
||||
let service: ServerlessService
|
||||
|
||||
beforeAll(async () => {
|
||||
await dbHelper.connect()
|
||||
const moduleRef = Test.createTestingModule({
|
||||
providers: [
|
||||
ServerlessService,
|
||||
HttpService,
|
||||
AssetService,
|
||||
{
|
||||
provide: CacheService,
|
||||
useValue: {},
|
||||
},
|
||||
|
||||
{
|
||||
provide: getModelToken('SnippetModel'),
|
||||
useValue: getModelForClass(SnippetModel),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const app = await moduleRef.compile()
|
||||
await app.init()
|
||||
service = app.get(ServerlessService)
|
||||
})
|
||||
|
||||
describe('run serverless function', () => {
|
||||
test('case-1', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return 1 + 1
|
||||
}.toString(),
|
||||
})
|
||||
const data = await service.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req: {} as any, res: {} as any },
|
||||
)
|
||||
expect(data).toBe(2)
|
||||
})
|
||||
|
||||
test('case-2: require built-in module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return (await require('path')).join('1', '1')
|
||||
}.toString(),
|
||||
})
|
||||
const data = await service.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req: {} as any, res: {} as any },
|
||||
)
|
||||
expect(data).toBe('1/1')
|
||||
})
|
||||
|
||||
test('case-3: require extend module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return (await require('axios')).get.toString()
|
||||
}.toString(),
|
||||
})
|
||||
const data = await service.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req: {} as any, res: {} as any },
|
||||
)
|
||||
expect(data).toBeDefined()
|
||||
})
|
||||
|
||||
test('case-4: require ban module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return await require('os')
|
||||
}.toString(),
|
||||
})
|
||||
|
||||
expect(
|
||||
service.injectContextIntoServerlessFunctionAndCall(model, {
|
||||
req: {} as any,
|
||||
res: {} as any,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('case-5: require ban extend module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return await require('@nestjs/core')
|
||||
}.toString(),
|
||||
})
|
||||
|
||||
expect(
|
||||
service.injectContextIntoServerlessFunctionAndCall(model, {
|
||||
req: {} as any,
|
||||
res: {} as any,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('case-6: throws', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return context.throws(404, 'not found')
|
||||
}.toString(),
|
||||
})
|
||||
|
||||
expect(
|
||||
service.injectContextIntoServerlessFunctionAndCall(model, {
|
||||
req: {} as any,
|
||||
res: createMockedContextResponse(),
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,12 +4,10 @@ import { getModelForClass } from '@typegoose/typegoose'
|
||||
import { getModelToken } from 'nestjs-typegoose'
|
||||
import { dbHelper } from 'test/helper/db-mock.helper'
|
||||
import { setupE2EApp } from 'test/helper/register-app.helper'
|
||||
import { ServerlessService } from '~/modules/serverless/serverless.service'
|
||||
import { SnippetController } from '~/modules/snippet/snippet.controller'
|
||||
import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model'
|
||||
import { SnippetService } from '~/modules/snippet/snippet.service'
|
||||
import { CacheService } from '~/processors/cache/cache.service'
|
||||
import { AssetService } from '~/processors/helper/helper.asset.service'
|
||||
import { HttpService } from '~/processors/helper/helper.http.service'
|
||||
|
||||
describe('test /snippets', () => {
|
||||
let app: NestFastifyApplication
|
||||
@@ -36,9 +34,15 @@ describe('test /snippets', () => {
|
||||
controllers: [SnippetController],
|
||||
providers: [
|
||||
SnippetService,
|
||||
AssetService,
|
||||
HttpService,
|
||||
{ provide: CacheService, useValue: {} },
|
||||
|
||||
{
|
||||
provide: ServerlessService,
|
||||
useValue: {
|
||||
isValidServerlessFunction() {
|
||||
return true
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getModelToken(SnippetModel.name),
|
||||
useValue: model,
|
||||
@@ -55,7 +59,7 @@ describe('test /snippets', () => {
|
||||
method: 'POST',
|
||||
url: '/snippets',
|
||||
payload: {
|
||||
name: 'Snippet-1',
|
||||
name: 'Snippet*1',
|
||||
private: false,
|
||||
raw: JSON.stringify({ foo: 'bar' }),
|
||||
type: SnippetType.JSON,
|
||||
@@ -124,4 +128,39 @@ describe('test /snippets', () => {
|
||||
expect(json).toStrictEqual(JSON.parse(mockPayload1.raw))
|
||||
})
|
||||
})
|
||||
|
||||
const snippetFuncType = {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return 1 + 1
|
||||
}.toString(),
|
||||
name: 'func-1',
|
||||
private: false,
|
||||
reference: 'root',
|
||||
}
|
||||
|
||||
test('POST /snippets, should create function successfully', async () => {
|
||||
await app
|
||||
.inject({
|
||||
method: 'POST',
|
||||
url: '/snippets',
|
||||
payload: {
|
||||
...snippetFuncType,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.statusCode).toBe(201)
|
||||
})
|
||||
})
|
||||
|
||||
test('GET /snippets/root/func-1', async () => {
|
||||
await app
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: '/snippets/root/func-1',
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.statusCode).toBe(400)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,12 +3,10 @@ import { Test } from '@nestjs/testing'
|
||||
import { getModelForClass } from '@typegoose/typegoose'
|
||||
import { getModelToken } from 'nestjs-typegoose'
|
||||
import { dbHelper } from 'test/helper/db-mock.helper'
|
||||
import { createMockedContextResponse } from '~/modules/snippet/mock-response.util'
|
||||
import { ServerlessService } from '~/modules/serverless/serverless.service'
|
||||
import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model'
|
||||
import { SnippetService } from '~/modules/snippet/snippet.service'
|
||||
import { CacheService } from '~/processors/cache/cache.service'
|
||||
import { AssetService } from '~/processors/helper/helper.asset.service'
|
||||
import { HttpService } from '~/processors/helper/helper.http.service'
|
||||
|
||||
describe('test Snippet Service', () => {
|
||||
let service: SnippetService
|
||||
@@ -18,11 +16,11 @@ describe('test Snippet Service', () => {
|
||||
const moduleRef = Test.createTestingModule({
|
||||
providers: [
|
||||
SnippetService,
|
||||
AssetService,
|
||||
HttpService,
|
||||
|
||||
{ provide: CacheService, useValue: {} },
|
||||
{ provide: ServerlessService, useValue: {} },
|
||||
{
|
||||
provide: getModelToken('SnippetModel'),
|
||||
provide: getModelToken(SnippetModel.name),
|
||||
useValue: getModelForClass(SnippetModel),
|
||||
},
|
||||
],
|
||||
@@ -44,6 +42,7 @@ describe('test Snippet Service', () => {
|
||||
private: false,
|
||||
reference: 'root',
|
||||
}
|
||||
|
||||
let id = ''
|
||||
it('should create one', async () => {
|
||||
const res = await service.create(snippet)
|
||||
@@ -68,104 +67,6 @@ describe('test Snippet Service', () => {
|
||||
expect(res.name).toBe(snippet.name)
|
||||
})
|
||||
|
||||
describe('run serverless function', () => {
|
||||
test('case-1', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return 1 + 1
|
||||
}.toString(),
|
||||
})
|
||||
const data = await service.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req: {} as any, res: {} as any },
|
||||
)
|
||||
expect(data).toBe(2)
|
||||
})
|
||||
|
||||
test('case-2: require built-in module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return (await require('path')).join('1', '1')
|
||||
}.toString(),
|
||||
})
|
||||
const data = await service.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req: {} as any, res: {} as any },
|
||||
)
|
||||
expect(data).toBe('1/1')
|
||||
})
|
||||
|
||||
test('case-3: require extend module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return (await require('axios')).get.toString()
|
||||
}.toString(),
|
||||
})
|
||||
const data = await service.injectContextIntoServerlessFunctionAndCall(
|
||||
model,
|
||||
{ req: {} as any, res: {} as any },
|
||||
)
|
||||
expect(data).toBeDefined()
|
||||
})
|
||||
|
||||
test('case-4: require ban module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return await require('os')
|
||||
}.toString(),
|
||||
})
|
||||
|
||||
expect(
|
||||
service.injectContextIntoServerlessFunctionAndCall(model, {
|
||||
req: {} as any,
|
||||
res: {} as any,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('case-5: require ban extend module', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return await require('@nestjs/core')
|
||||
}.toString(),
|
||||
})
|
||||
|
||||
expect(
|
||||
service.injectContextIntoServerlessFunctionAndCall(model, {
|
||||
req: {} as any,
|
||||
res: {} as any,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('case-6: throws', async () => {
|
||||
const model = new SnippetModel()
|
||||
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
|
||||
type: SnippetType.Function,
|
||||
raw: async function handler(context, require) {
|
||||
return context.throws(404, 'not found')
|
||||
}.toString(),
|
||||
})
|
||||
|
||||
expect(
|
||||
service.injectContextIntoServerlessFunctionAndCall(model, {
|
||||
req: {} as any,
|
||||
res: createMockedContextResponse(),
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
test('modify', async () => {
|
||||
const newSnippet = {
|
||||
name: 'test',
|
||||
|
||||
Reference in New Issue
Block a user