feat: init serverless function
This commit is contained in:
@@ -67,7 +67,7 @@ export class SnippetModel extends BaseModel {
|
||||
@IsOptional()
|
||||
comment?: string
|
||||
|
||||
// 类型注释
|
||||
// 元数据类型 (预留二级类型,暂时不用)
|
||||
@prop({ maxlength: 20 })
|
||||
@MaxLength(20)
|
||||
@IsString()
|
||||
|
||||
@@ -41,4 +41,14 @@ output:
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Serverless Function
|
||||
|
||||
```js
|
||||
|
||||
function handle() {}
|
||||
|
||||
```
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@nestjs/common'
|
||||
import { load } from 'js-yaml'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { safeEval } from '~/utils/safe-eval.util'
|
||||
import { SnippetModel, SnippetType } from './snippet.model'
|
||||
@Injectable()
|
||||
export class SnippetService {
|
||||
@@ -60,11 +61,13 @@ export class SnippetService {
|
||||
}
|
||||
break
|
||||
}
|
||||
case SnippetType.Function:
|
||||
// TODO
|
||||
throw new BadRequestException(
|
||||
'Serverless functions are not currently supported',
|
||||
)
|
||||
case SnippetType.Function: {
|
||||
const isValid = await this.isValidServerlessFunction(model.raw)
|
||||
if (!isValid) {
|
||||
throw new BadRequestException('serverless function is not valid')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case SnippetType.Text:
|
||||
default: {
|
||||
@@ -73,6 +76,22 @@ export class SnippetService {
|
||||
}
|
||||
}
|
||||
|
||||
async injectContextIntoServerlessFunctionAndCall(functionString: string) {
|
||||
return {}
|
||||
}
|
||||
|
||||
async isValidServerlessFunction(raw: string) {
|
||||
try {
|
||||
return safeEval(`
|
||||
${raw}
|
||||
// 验证 handler 是否存在并且是函数
|
||||
return typeof handler === 'function'
|
||||
`)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getSnippetById(id: string) {
|
||||
const doc = await this.model.findById(id).lean()
|
||||
return this.attachSnippet(doc)
|
||||
@@ -106,6 +125,10 @@ export class SnippetService {
|
||||
Reflect.set(model, 'data', model.raw)
|
||||
break
|
||||
}
|
||||
|
||||
case SnippetType.Function: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return model as SnippetModel & { data: any }
|
||||
|
||||
34
src/utils/safe-eval.util.ts
Normal file
34
src/utils/safe-eval.util.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import vm from 'vm'
|
||||
const nanoid = customAlphabet(
|
||||
'0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$',
|
||||
16,
|
||||
)
|
||||
export function safeEval(
|
||||
code: string,
|
||||
context = {},
|
||||
opts?: string | vm.RunningScriptOptions,
|
||||
) {
|
||||
const sandbox = {}
|
||||
const resultKey = 'SAFE_EVAL_' + nanoid()
|
||||
sandbox[resultKey] = {}
|
||||
const clearContext = `
|
||||
(function() {
|
||||
Function = undefined;
|
||||
const keys = Object.getOwnPropertyNames(this).concat(['constructor']);
|
||||
keys.forEach((key) => {
|
||||
const item = this[key];
|
||||
if (!item || typeof item.constructor !== 'function') return;
|
||||
this[key].constructor = undefined;
|
||||
});
|
||||
})();
|
||||
`
|
||||
code = clearContext + resultKey + '=' + `((() => { ${code} })())`
|
||||
if (context) {
|
||||
Object.keys(context).forEach(function (key) {
|
||||
sandbox[key] = context[key]
|
||||
})
|
||||
}
|
||||
vm.runInNewContext(code, sandbox, opts)
|
||||
return sandbox[resultKey]
|
||||
}
|
||||
55
test/src/utils/safe-eval.spec.ts
Normal file
55
test/src/utils/safe-eval.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { safeEval } from '~/utils/safe-eval.util'
|
||||
|
||||
describe.only('test safe-eval', () => {
|
||||
it('should eval', () => {
|
||||
const res = safeEval(`return 1 + 2`)
|
||||
expect(res).toBe(3)
|
||||
})
|
||||
|
||||
it('should eval with ctx', () => {
|
||||
const res = safeEval(`return a + b`, { a: 1, b: 2 })
|
||||
expect(res).toBe(3)
|
||||
})
|
||||
|
||||
it('should can not access to global or process or require', () => {
|
||||
expect(() => {
|
||||
safeEval(`return global`)
|
||||
}).toThrow()
|
||||
|
||||
expect(() => {
|
||||
safeEval(`return process`)
|
||||
}).toThrow()
|
||||
|
||||
expect(() => {
|
||||
safeEval(`return require`)
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
it('should can access mocked global context', () => {
|
||||
const res = safeEval(`return global.a`, { global: { a: 1 } })
|
||||
expect(res).toBe(1)
|
||||
})
|
||||
it('should can access mocked require function', () => {
|
||||
const res = safeEval(`return require('fs').readFileSync('/etc/hosts')`, {
|
||||
require: (name: string) => {
|
||||
if (name === 'fs') {
|
||||
return {
|
||||
readFileSync: (path: string) => {
|
||||
return ''
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
expect(res).toBe('')
|
||||
})
|
||||
|
||||
it('should handle promise', async () => {
|
||||
const promise = safeEval(
|
||||
`async function handler() { return 'Hello' }; return handler()`,
|
||||
)
|
||||
expect(promise.__proto__.constructor.name).toBe('Promise')
|
||||
|
||||
expect(await promise).toBe('Hello')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user