Skip to content

Commit b93c8e2

Browse files
committed
feat(core): add default evaluation service
1 parent 0e1167c commit b93c8e2

File tree

6 files changed

+174
-101
lines changed

6 files changed

+174
-101
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export default class EvaluationOutput {
2+
public error: any | null
3+
public result: any | null
4+
public logs: string[]
5+
6+
constructor(props: EvaluationOutput) {
7+
this.error = props.error
8+
this.result = props.result
9+
this.logs = props.logs
10+
}
11+
12+
public static error(error: any, logs: EvaluationOutput['logs'] = []) {
13+
return new EvaluationOutput({
14+
error,
15+
logs,
16+
result: null,
17+
})
18+
}
19+
20+
public static formatLog(arg: any) {
21+
if (typeof arg === 'string') {
22+
return arg
23+
}
24+
25+
return JSON.stringify(arg)
26+
}
27+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { test } from '@japa/runner'
2+
import IEvaluationService from './evaluation'
3+
import DefaultEvaluation from './implementations/default-evaluation'
4+
import NodeVMEvaluation from './implementations/node-vm-evaluation'
5+
6+
interface Services {
7+
name: string
8+
service: IEvaluationService
9+
}
10+
11+
test.group('evaluation (unit)', () => {
12+
const services: Services[] = [
13+
{ name: 'default', service: new DefaultEvaluation() },
14+
{ name: 'node-vm', service: new NodeVMEvaluation() },
15+
]
16+
17+
test('should "{name}" execute script and return result ')
18+
.with(services)
19+
.run(async ({ expect }, { service }) => {
20+
const result = await service.evaluate('setResult(4)')
21+
22+
expect(result.result).toBe(4)
23+
})
24+
25+
test('should "{name}" execute Promise script and set result')
26+
.with(services)
27+
.run(async ({ expect }, { service }) => {
28+
const result = await service.evaluate(`
29+
const main = () => Promise.resolve(4)
30+
31+
setResult(await main())
32+
`)
33+
34+
expect(result.result).toBe(4)
35+
})
36+
37+
test('should "{name}" execute script with scope')
38+
.with(services)
39+
.run(async ({ expect }, { service }) => {
40+
let data = {}
41+
42+
const scope = {
43+
setData: (arg: any) => (data = arg),
44+
}
45+
46+
await service.evaluate('setData([1,2,3])', scope)
47+
48+
expect(data).toEqual([1, 2, 3])
49+
})
50+
51+
test('should "{name}" return error if have one')
52+
.with(services)
53+
.run(async ({ expect }, { service }) => {
54+
const result = await service.evaluate(`
55+
const main = () => Promise.reject('Error in script')
56+
57+
setResult(await main())
58+
`)
59+
60+
expect(result.error).toEqual('Error in script')
61+
})
62+
63+
test('should "{name}" execute script and return logs')
64+
.with(services)
65+
.run(async ({ expect }, { service }) => {
66+
const result = await service.evaluate(`
67+
console.log('Hello')
68+
console.log({ key: 123 })
69+
console.log([ { key: 'abc' } ])
70+
`)
71+
72+
expect(result.logs).toEqual(['Hello', '{"key":123}', '[{"key":"abc"}]'])
73+
})
74+
75+
test('should "{name}" not be able to use import')
76+
.with(services)
77+
.run(async ({ expect }, { service }) => {
78+
const result = await service.evaluate(`import fs from 'fs'`)
79+
80+
expect(result.error.message).toBe('Cannot use import statement outside a module')
81+
})
82+
83+
test('should "{name}" not be able to use require')
84+
.with(services)
85+
.run(async ({ expect }, { service }) => {
86+
const result = await service.evaluate(`const fs = require('fs')`)
87+
88+
expect(result.error.message).toBe('require is not defined')
89+
})
90+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import EvaluationOutput from '../../../entities/evaluation-output'
2+
import IEvaluationService from '../evaluation'
3+
import escapeRegExp from 'lodash/escapeRegExp'
4+
5+
export default class DefaultEvaluation implements IEvaluationService {
6+
protected async _evaluate(code: string, scope?: Record<string, any>) {
7+
const logs: string[] = []
8+
let result = null
9+
const error = null
10+
11+
const sandbox = {
12+
...scope,
13+
setResult: (data: any) => (result = data),
14+
console: {
15+
log: (...args: any) => logs.push(args.map(EvaluationOutput.formatLog).join(' ')),
16+
},
17+
}
18+
19+
let fixCode = code
20+
21+
for (const key in sandbox) {
22+
fixCode = fixCode
23+
.replace(new RegExp(escapeRegExp(`${key}.`), 'g'), `context.${key}.`)
24+
.replace(new RegExp(escapeRegExp(`${key}(`), 'g'), `context.${key}(`)
25+
}
26+
27+
const fn = new Function(`
28+
const context = this
29+
30+
async function run(){
31+
${fixCode}
32+
}
33+
34+
return run()
35+
`)
36+
37+
await fn.bind(sandbox)()
38+
39+
return {
40+
result,
41+
error,
42+
logs,
43+
}
44+
}
45+
46+
public async evaluate(code: string, scope?: Record<string, any>) {
47+
try {
48+
return await this._evaluate(code, scope)
49+
} catch (error: any) {
50+
return EvaluationOutput.error(error)
51+
}
52+
}
53+
}

packages/core/gateways/evaluation/implementations/node-vm-evaluation.spec.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
11
import vm from 'vm'
2-
import util from 'util'
32

4-
import * as ts from 'typescript'
5-
6-
function tsCompile(source: string, options: ts.TranspileOptions = {}): string {
7-
// Default options -- you could also perform a merge, or use the project tsconfig.json
8-
if (null === options) {
9-
options = { compilerOptions: { module: ts.ModuleKind.CommonJS } }
10-
}
11-
return ts.transpileModule(source, options).outputText
12-
}
13-
14-
const format = (args: any) => {
15-
if (typeof args === 'string') {
16-
return args
17-
}
18-
19-
return util.inspect(args)
20-
}
3+
import EvaluationOutput from '../../../entities/evaluation-output'
214

225
export default class ScriptService {
236
protected async _evaluate(code: string, scope?: Record<string, any>) {
@@ -28,13 +11,13 @@ export default class ScriptService {
2811
const sandbox = {
2912
setResult: (data: any) => (result = data),
3013
console: {
31-
log: (...args: any) => logs.push(args.map(format).join(' ')),
14+
log: (...args: any) => logs.push(args.map(EvaluationOutput.formatLog).join(' ')),
3215
},
3316
...scope,
3417
main: (): Promise<any> => Promise.resolve('Error executing script'),
3518
}
3619

37-
const executable = new vm.Script(`async function main(){ ${tsCompile(code)} }`)
20+
const executable = new vm.Script(`async function main(){ ${code} }`)
3821
const context = vm.createContext(sandbox)
3922

4023
executable.runInContext(context, {})
@@ -52,14 +35,7 @@ export default class ScriptService {
5235
try {
5336
return await this._evaluate(code, scope)
5437
} catch (error: any) {
55-
return {
56-
result: null,
57-
error: {
58-
message: error.message,
59-
stack: error.stack,
60-
},
61-
logs: [],
62-
}
38+
return EvaluationOutput.error(error)
6339
}
6440
}
6541
}

packages/core/use-cases/execute-script/execute-script.spec.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { test } from '@japa/runner'
22
import DirectoryEntry from '../../entities/directory-entry'
33
import Workspace from '../../entities/workspace'
44

5-
import InMemoryApp from '../../__tests__/app'
65
import InMemoryAppConfig from '../../__tests__/in-memory-config'
76
import ExecuteScript from './execute-script'
87

@@ -27,24 +26,6 @@ test.group('execute-script (use-case)', (group) => {
2726
expect(result).toBe('Hello word')
2827
})
2928

30-
test('should throw an error when trying to use require("fs")', async ({ expect }) => {
31-
const result = await useCase.execute({
32-
workspaceId: workspace.id,
33-
content: 'const fs = require("fs")',
34-
})
35-
36-
expect(result.error.message).toBe('require is not defined')
37-
})
38-
39-
test('should throw an error when trying to use import fs from "fs"', async ({ expect }) => {
40-
const result = await useCase.execute({
41-
workspaceId: workspace.id,
42-
content: `import fs from "fs"`,
43-
})
44-
45-
expect(result.error.message).toBe('exports is not defined')
46-
})
47-
4829
test('should create a file in workspace', async ({ expect }) => {
4930
await useCase.execute({
5031
workspaceId: workspace.id,

0 commit comments

Comments
 (0)