From 9c969aff370eaed746f9707307798f2678152806 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Mon, 29 Jun 2020 16:10:27 +0200 Subject: [PATCH 01/15] feat: Add first version of MapInterpreter --- .eslintrc.js | 1 + .npmrc | 1 + package.json | 3 +- src/client/interpreter/Sandbox.ts | 20 +- src/index.test.ts | 5 - src/internal/http.test.ts | 63 ++ src/internal/http.ts | 51 ++ src/internal/interpreter/interfaces.ts | 94 +++ .../interpreter/map-interpreter.test.ts | 660 ++++++++++++++++++ src/internal/interpreter/map-interpreter.ts | 362 ++++++++++ tsconfig.json | 1 + yarn.lock | 17 + 12 files changed, 1269 insertions(+), 9 deletions(-) create mode 100644 .npmrc delete mode 100644 src/index.test.ts create mode 100644 src/internal/http.test.ts create mode 100644 src/internal/http.ts create mode 100644 src/internal/interpreter/interfaces.ts create mode 100644 src/internal/interpreter/map-interpreter.test.ts create mode 100644 src/internal/interpreter/map-interpreter.ts diff --git a/.eslintrc.js b/.eslintrc.js index d7ffbc94..de643e28 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,7 @@ module.exports = { 'import/newline-after-import': 'error', 'import/no-duplicates': 'error', 'no-multiple-empty-lines': 'error', + 'lines-between-class-members': 'error', }, settings: { 'import/parsers': { diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..ac891f80 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@superindustries:registry=https://npm.pkg.github.com diff --git a/package.json b/package.json index e67b3217..4f3431f2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "types": "dist/index.d.ts", "repository": "https://github.com/superindustries/superface.git", "author": "Superface Team", - "license": "MIT", "private": false, "scripts": { "build": "microbundle --tsconfig tsconfig.release.json", @@ -40,6 +39,8 @@ "typescript": "^3.9.2" }, "dependencies": { + "cross-fetch": "^3.0.5", + "@superindustries/language": "^0.0.7", "vm2": "^3.9.2" } } diff --git a/src/client/interpreter/Sandbox.ts b/src/client/interpreter/Sandbox.ts index c9391ee3..e0248cbd 100644 --- a/src/client/interpreter/Sandbox.ts +++ b/src/client/interpreter/Sandbox.ts @@ -1,7 +1,10 @@ -import { VM } from "vm2"; +import { VM } from 'vm2'; export class Sandbox { - evalJS = (js: string): unknown => { + evalJS = ( + js: string, + variableDefinitions?: Record + ): unknown => { const vm = new VM({ sandbox: {}, wasm: false, @@ -9,6 +12,17 @@ export class Sandbox { timeout: 100, }); - return vm.run(`'use strict'; ${js}`); + let variables = ''; + + if (variableDefinitions) { + variables = Object.entries(variableDefinitions) + .map( + ([key, value]) => + `const ${key} = JSON.parse('${JSON.stringify(value)}');` + ) + .join(''); + } + + return vm.run(`'use strict';${variables}${js}`); }; } diff --git a/src/index.test.ts b/src/index.test.ts deleted file mode 100644 index 61529d31..00000000 --- a/src/index.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('sanity check', () => { - it('works', () => { - expect(true).toBeTruthy(); - }); -}); diff --git a/src/internal/http.test.ts b/src/internal/http.test.ts new file mode 100644 index 00000000..e1dc4d9d --- /dev/null +++ b/src/internal/http.test.ts @@ -0,0 +1,63 @@ +import { + createServer, + IncomingMessage, + RequestListener, + ServerResponse, +} from 'http'; + +import { HttpClient } from './http'; + +const port = Math.floor(Math.random() * 64511 + 1024); + +const listener: RequestListener = ( + req: IncomingMessage, + res: ServerResponse +) => { + switch (`${req.method} ${req.url}`) { + case 'GET /valid': + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify({ response: 'valid' })); + break; + + case 'GET /invalid': + res.writeHead(404); + res.write(JSON.stringify({ error: { message: 'Not found' } })); + break; + + default: + throw new Error( + `Invalid combination of url and method: ${req.url}, ${req.method}` + ); + } + res.end(); +}; + +const server = createServer(listener); + +describe('HttpClient', () => { + beforeAll(() => { + server.listen(port); + }); + + afterAll(() => { + server.close(); + }); + + it('gets basic response', async () => { + const response = await HttpClient.request( + `http://localhost:${port}/valid`, + { method: 'get' } + ); + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({ response: 'valid' }); + }); + + it('gets error response', async () => { + const response = await HttpClient.request( + `http://localhost:${port}/invalid`, + { method: 'get' } + ); + expect(response.statusCode).toEqual(404); + expect(response.body).toEqual({ error: { message: 'Not found' } }); + }); +}); diff --git a/src/internal/http.ts b/src/internal/http.ts new file mode 100644 index 00000000..5ef29634 --- /dev/null +++ b/src/internal/http.ts @@ -0,0 +1,51 @@ +import fetch from 'cross-fetch'; + +export interface HttpResponse { + statusCode: number; + body: unknown; +} + +export const HttpClient = { + request: async ( + url: string, + parameters: { + method: string; + headers?: Record; + queryParameters?: Record; + body?: unknown; + } + ): Promise => { + let query = ''; + + if ( + parameters?.queryParameters && + Object.keys(parameters.queryParameters).length + ) { + query = + '?' + + Object.entries(parameters.queryParameters) + .map(([key, value]) => `${key}=${value}`) + .join('&'); + } + + const params: RequestInit = { + headers: { 'Content-Type': 'application/json', ...parameters?.headers }, + method: parameters.method, + }; + + if ( + parameters.method !== 'GET' && + parameters.method !== 'HEAD' && + parameters.body + ) { + params.body = JSON.stringify(parameters.body); + } + + const response = await fetch(encodeURI(`${url}${query}`), params); + + return { + statusCode: response.status, + body: await response.json(), + }; + }, +}; diff --git a/src/internal/interpreter/interfaces.ts b/src/internal/interpreter/interfaces.ts new file mode 100644 index 00000000..04e28a68 --- /dev/null +++ b/src/internal/interpreter/interfaces.ts @@ -0,0 +1,94 @@ +import { + EvalDefinitionNode, + HTTPOperationDefinitionNode, + IterationDefinitionNode, + JSExpressionNode, + MapASTNode, + MapDefinitionNode, + MapDocumentNode, + MapExpressionDefinitionNode, + MapNode, + NetworkOperationDefinitionNode, + OperationCallDefinitionNode, + OperationDefinitionNode, + OutcomeDefinitionNode, + ProfileIdNode, + ProviderNode, + StepDefinitionNode, + VariableExpressionDefinitionNode, +} from '@superindustries/language'; + +export interface MapParameters { + usecase?: string; +} + +export interface MapVisitor { + visit( + node: MapASTNode, + parameters: MapParameters + ): Promise | unknown; + visitEvalDefinitionNode( + node: EvalDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitHTTPOperationDefinitionNode( + node: HTTPOperationDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitIterationDefinitionNode( + node: IterationDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitJSExpressionNode( + node: JSExpressionNode, + parameters: MapParameters + ): Promise | unknown; + visitMapDefinitionNode( + node: MapDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitMapDocumentNode( + node: MapDocumentNode, + parameters: MapParameters + ): Promise | unknown; + visitMapExpressionDefinitionNode( + node: MapExpressionDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitMapNode( + node: MapNode, + parameters: MapParameters + ): Promise | unknown; + visitNetworkOperationDefinitionNode( + node: NetworkOperationDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitOperationCallDefinitionNode( + node: OperationCallDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitOperationDefinitionNode( + node: OperationDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitOutcomeDefinitionNode( + node: OutcomeDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitProfileIdNode( + node: ProfileIdNode, + parameters: MapParameters + ): Promise | unknown; + visitProviderNode( + node: ProviderNode, + parameters: MapParameters + ): Promise | unknown; + visitStepDefinitionNode( + node: StepDefinitionNode, + parameters: MapParameters + ): Promise | unknown; + visitVariableExpressionDefinitionNode( + node: VariableExpressionDefinitionNode, + parameters: MapParameters + ): Promise | unknown; +} diff --git a/src/internal/interpreter/map-interpreter.test.ts b/src/internal/interpreter/map-interpreter.test.ts new file mode 100644 index 00000000..c12a6231 --- /dev/null +++ b/src/internal/interpreter/map-interpreter.test.ts @@ -0,0 +1,660 @@ +import { MapASTNode } from '@superindustries/language'; +import { + createServer, + IncomingMessage, + RequestListener, + ServerResponse, +} from 'http'; +import { StringDecoder } from 'string_decoder'; + +import { MapInterpereter } from './map-interpreter'; + +const port = Math.floor(Math.random() * 64511 + 1024); + +const listener: RequestListener = ( + req: IncomingMessage, + res: ServerResponse +) => { + const decoder = new StringDecoder('utf-8'); + let buffer = ''; + + switch (`${req.method} ${req.url}`) { + case 'GET /twelve': + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify({ data: 12 })); + res.end(); + break; + + case 'GET /twelve?page=2': + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify({ data: 144 })); + res.end(); + break; + + case 'POST /checkBody': + req.on('data', data => (buffer += decoder.write(data))); + + req.on('end', () => { + buffer += decoder.end(); + const body = JSON.parse(buffer); + res.writeHead(201, { 'Content-Type': 'application/json' }); + res.write( + JSON.stringify({ + bodyOk: body.anArray.length === 3, + headerOk: req.headers['someheader'] === 'hello', + }) + ); + res.end(); + }); + break; + + default: + throw new Error( + `Invalid combination of url and method: ${req.url}, ${req.method}` + ); + } +}; + +const server = createServer(listener); + +describe('MapInterpreter', () => { + let interpreter: MapInterpereter; + + beforeAll(() => { + server.listen(port); + }); + + afterAll(() => { + server.close(); + }); + + beforeEach(() => { + interpreter = new MapInterpereter(); + }); + + it('should fail with invalid AST', async () => { + await expect( + async () => + await interpreter.visit( + ({ kind: 'Invalid' } as unknown) as MapASTNode, + {} + ) + ).rejects.toThrow(); + }); + + it('should execute minimal Eval definition', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: '12', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should execute Eval definition with variables', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'x', + right: { + kind: 'JSExpression', + expression: '7', + }, + }, + ], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'x + 5', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should correctly resolve variable scope', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'x', + right: { + kind: 'JSExpression', + expression: '8', + }, + }, + ], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'x', + right: { + kind: 'JSExpression', + expression: '7', + }, + }, + ], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'x + 5', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should run predefined operation', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'OperationCallDefinition', + arguments: [], + operationName: 'my beloved operation', + successOutcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'variableFromOperation', + }, + }, + ], + }, + }, + }, + ], + }, + { + kind: 'OperationDefinition', + operationName: 'my beloved operation', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'step', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'variableFromOperation', + right: { + kind: 'JSExpression', + expression: '12', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should call an API', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `http://localhost:${port}/twelve`, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en_US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'response.data', + }, + }, + ], + }, + }, + requestDefinition: { + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should call an API with parameters', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'pageNumber', + right: { + kind: 'JSExpression', + expression: '2', + }, + }, + ], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `http://localhost:${port}/twelve`, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en_US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'response.data', + }, + }, + ], + }, + }, + requestDefinition: { + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'page', + right: { + kind: 'JSExpression', + expression: 'pageNumber', + }, + }, + ], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 144 }); + }); + + it('should call an API with parameters and POST request', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `http://localhost:${port}/checkBody`, + method: 'POST', + responseDefinition: { + statusCode: 201, + contentType: 'application/json', + contentLanguage: 'en_US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'response', + }, + }, + ], + }, + }, + requestDefinition: { + body: [ + { + kind: 'MapExpressionsDefinition', + left: 'anArray', + right: { + kind: 'JSExpression', + expression: '[1, 2, 3]', + }, + }, + ], + headers: [ + { + kind: 'VariableExpressionsDefinition', + left: 'SomeHeader', + right: { + kind: 'JSExpression', + expression: '"hello"', + }, + }, + ], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ + result: { + headerOk: true, + bodyOk: true, + }, + }); + }); +}); diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts new file mode 100644 index 00000000..54c9b64c --- /dev/null +++ b/src/internal/interpreter/map-interpreter.ts @@ -0,0 +1,362 @@ +import { + EvalDefinitionNode, + HTTPOperationDefinitionNode, + isMapDefinitionNode, + isOperationDefinitionNode, + IterationDefinitionNode, + JSExpressionNode, + MapASTNode, + MapDefinitionNode, + MapDocumentNode, + MapExpressionDefinitionNode, + MapNode, + NetworkOperationDefinitionNode, + OperationCallDefinitionNode, + OperationDefinitionNode, + OutcomeDefinitionNode, + ProfileIdNode, + ProviderNode, + StepDefinitionNode, + VariableExpressionDefinitionNode, +} from '@superindustries/language'; + +import { Sandbox } from '../../client/interpreter/Sandbox'; +import { HttpClient } from '../http'; +import { MapParameters, MapVisitor } from './interfaces'; + +function assertUnreachable(_node: never): never; +function assertUnreachable(node: MapASTNode): never { + throw new Error(`Invalid Node kind: ${node.kind}`); +} + +export class MapInterpereter implements MapVisitor { + private variableStack: Record[] = []; + + private operations: OperationDefinitionNode[] = []; + + private scopedVariables: Record> = {}; + + private operationScope: string | undefined; + + async visit(node: MapASTNode, parameters: MapParameters): Promise { + switch (node.kind) { + case 'EvalDefinition': + return await this.visitEvalDefinitionNode(node, parameters); + case 'HTTPOperationDefinition': + return this.visitHTTPOperationDefinitionNode(node, parameters); + case 'IterationDefinition': + return this.visitIterationDefinitionNode(node, parameters); + case 'JSExpression': + return this.visitJSExpressionNode(node, parameters); + case 'Map': + return this.visitMapNode(node, parameters); + case 'MapDefinition': + return this.visitMapDefinitionNode(node, parameters); + case 'MapDocument': + return this.visitMapDocumentNode(node, parameters); + case 'MapExpressionsDefinition': + return this.visitMapExpressionDefinitionNode(node, parameters); + case 'NetworkOperationDefinition': + return this.visitNetworkOperationDefinitionNode(node, parameters); + case 'OperationCallDefinition': + return this.visitOperationCallDefinitionNode(node, parameters); + case 'OperationDefinition': + return this.visitOperationDefinitionNode(node, parameters); + case 'OutcomeDefinition': + return this.visitOutcomeDefinitionNode(node, parameters); + case 'ProfileId': + return this.visitProfileIdNode(node, parameters); + case 'Provider': + return this.visitProviderNode(node, parameters); + case 'StepDefinition': + return this.visitStepDefinitionNode(node, parameters); + case 'VariableExpressionsDefinition': + return this.visitVariableExpressionDefinitionNode(node, parameters); + + default: + assertUnreachable(node); + } + } + + async visitEvalDefinitionNode( + node: EvalDefinitionNode, + parameters: MapParameters + ): Promise { + return this.visit(node.outcomeDefinition, parameters); + } + + async visitHTTPOperationDefinitionNode( + node: HTTPOperationDefinitionNode, + parameters: MapParameters + ): Promise { + const variables = await this.processVariableExpressions( + node.variableExpressionsDefinition, + parameters + ); + this.variableStack.push(variables); + + const queryParameters = await this.processVariableExpressions( + node.requestDefinition.queryParametersDefinition, + parameters + ); + + const body = await this.processMapExpressions( + node.requestDefinition.body, + parameters + ); + + const headers = await this.processVariableExpressions( + node.requestDefinition.headers, + parameters + ); + + const response = await HttpClient.request(node.url, { + queryParameters, + method: node.method, + body, + headers, + }); + + this.variableStack.push({ response: response.body as string }); + + return await this.visit( + node.responseDefinition.outcomeDefinition, + parameters + ); + } + + visitIterationDefinitionNode( + _node: IterationDefinitionNode, + _parameters: MapParameters + ): Promise | unknown { + throw new Error('Method not implemented.'); + } + + async visitJSExpressionNode( + node: JSExpressionNode, + _parameters: MapParameters + ): Promise { + const sandbox = new Sandbox(); + + return await sandbox.evalJS(node.expression, this.variables); + } + + async visitMapDefinitionNode( + node: MapDefinitionNode, + parameters: MapParameters + ): Promise { + const viableSteps = node.stepsDefinition.filter(async stepDefinition => { + return await this.visit(stepDefinition.condition, parameters); + }); + + if (viableSteps.length < 1) { + throw new Error('No step satisfies condition!'); + } + + const variables = await this.processVariableExpressions( + node.variableExpressionsDefinition, + parameters + ); + + this.variableStack.push(variables); + const result = await this.visit(viableSteps[0], parameters); + this.variableStack.pop(); + + return result; + } + + async visitMapDocumentNode( + node: MapDocumentNode, + parameters: MapParameters + ): Promise { + this.operations = node.definitions.filter(isOperationDefinitionNode); + + const operation = node.definitions + .filter(isMapDefinitionNode) + .find(definition => definition.usecaseName === parameters.usecase); + + if (!operation) { + throw new Error('Operation not found'); + } + + return await this.visit(operation, parameters); + } + + async visitMapExpressionDefinitionNode( + node: MapExpressionDefinitionNode, + parameters: MapParameters + ): Promise { + return { + [node.left]: (await this.visit(node.right, parameters)) as string, + }; + } + + visitMapNode( + _node: MapNode, + _parameters: MapParameters + ): Promise | unknown { + throw new Error('Method not implemented.'); + } + + visitNetworkOperationDefinitionNode( + node: NetworkOperationDefinitionNode, + parameters: MapParameters + ): Promise | unknown { + return this.visit(node.definition, parameters); + } + + async visitOperationCallDefinitionNode( + node: OperationCallDefinitionNode, + parameters: MapParameters + ): Promise { + const operation = this.operations.find( + operation => operation.operationName === node.operationName + ); + + if (!operation) { + throw new Error(`Operation ${node.operationName} not found!`); + } + + this.operationScope = operation.operationName; + let result = await this.visit(operation, parameters); + + if (!result) { + result = await this.visit(node.successOutcomeDefinition, parameters); + } + + this.operationScope = undefined; + + return result; + } + + async visitOperationDefinitionNode( + node: OperationDefinitionNode, + parameters: MapParameters + ): Promise { + const viableSteps = node.stepsDefinition.filter(async stepDefinition => { + return await this.visit(stepDefinition.condition, parameters); + }); + + if (viableSteps.length < 1) { + throw new Error('No step satisfies condition!'); + } + + const result = await this.visit(viableSteps[0], parameters); + + return result; + } + + async visitOutcomeDefinitionNode( + node: OutcomeDefinitionNode, + parameters: MapParameters + ): Promise { + if (node.returnDefinition) { + return await this.processMapExpressions( + node.returnDefinition, + parameters + ); + } else if (node.setDefinition) { + if (!this.operationScope) { + throw new Error( + 'Something very wrong happened. How did you even get here?' + ); + } + this.scopedVariables[ + this.operationScope + ] = await this.processVariableExpressions(node.setDefinition, parameters); + + return undefined; + } else if (node.resultDefinition) { + return await this.processMapExpressions( + node.resultDefinition, + parameters + ); + } else { + throw new Error('Something went very wrong, this should not happen!'); + } + } + + visitProfileIdNode( + _node: ProfileIdNode, + _parameters: MapParameters + ): Promise | unknown { + throw new Error('Method not implemented.'); + } + + visitProviderNode( + _node: ProviderNode, + _parameters: MapParameters + ): Promise | unknown { + throw new Error('Method not implemented.'); + } + + async visitStepDefinitionNode( + node: StepDefinitionNode, + parameters: MapParameters + ): Promise { + const variables = await this.processVariableExpressions( + node.variableExpressionsDefinition, + parameters + ); + + this.variableStack.push(variables); + const result = await this.visit(node.run, parameters); + this.variableStack.pop(); + + return result; + } + + async visitVariableExpressionDefinitionNode( + node: VariableExpressionDefinitionNode, + parameters: MapParameters + ): Promise { + return { + [node.left]: (await this.visit(node.right, parameters)) as string, + }; + } + + private get variables(): Record { + let variables = this.variableStack.reduce( + (acc, variableDefinition) => ({ + ...acc, + ...variableDefinition, + }), + {} + ); + + if (this.operationScope && this.scopedVariables[this.operationScope]) { + variables = { + ...variables, + ...this.scopedVariables[this.operationScope], + }; + } + + return variables; + } + + private async processVariableExpressions( + expressions: VariableExpressionDefinitionNode[], + parameters: MapParameters + ): Promise> { + return expressions.reduce( + async (acc, expression) => ({ + ...acc, + ...((await this.visit(expression, parameters)) as {}), + }), + Promise.resolve({}) + ); + } + + private async processMapExpressions( + expressions: MapExpressionDefinitionNode[], + parameters: MapParameters + ): Promise> { + return expressions.reduce( + async (acc, expression) => ({ + ...acc, + ...((await this.visit(expression, parameters)) as {}), + }), + Promise.resolve({}) + ); + } +} diff --git a/tsconfig.json b/tsconfig.json index 2f25ec1e..a4803380 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "declaration": true, "downlevelIteration": true, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "lib": [], "module": "ESNext", diff --git a/yarn.lock b/yarn.lock index 7dce95d1..fd469a8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1106,6 +1106,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@superindustries/language@^0.0.7": + version "0.0.7" + resolved "https://npm.pkg.github.com/download/@superindustries/language/0.0.7/bc74553cadd86b344d6cfbaa1e91e9f803a6695dfe4a96d3c62019c1975fe319#7c36984021ad4bfd93d0ad2dc315af583ccfc301" + integrity sha512-QpuyweFofet5TUGwPGLBLdYAeELxMpZ3Wa9xzQSjT2UC31RWVFankN5u8ng+WxmBT8NVl6VCAp2P3B6O+7QFNw== + "@types/babel__core@^7.1.7": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -2009,6 +2014,13 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +cross-fetch@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c" + integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew== + dependencies: + node-fetch "2.6.0" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4474,6 +4486,11 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-fetch@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" From d0aa23ab1678d366e4435dc3ef7c2d2645f05c7d Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Mon, 29 Jun 2020 17:28:26 +0200 Subject: [PATCH 02/15] chore: Fix Sandbox after merge --- src/client/interpreter/Sandbox.ts | 14 ++++---------- src/internal/interpreter/map-interpreter.ts | 6 ++---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/client/interpreter/Sandbox.ts b/src/client/interpreter/Sandbox.ts index b8480ecc..5b366e28 100644 --- a/src/client/interpreter/Sandbox.ts +++ b/src/client/interpreter/Sandbox.ts @@ -7,7 +7,9 @@ export function evalScript( variableDefinitions?: Record ): unknown { const vm = new VM({ - sandbox: {}, + sandbox: { + ...variableDefinitions, + }, compiler: 'javascript', wasm: false, eval: false, @@ -53,13 +55,5 @@ export function evalScript( ` ); - let variables = ''; - - if (variableDefinitions) { - variables = Object.entries(variableDefinitions) - .map(([key, value]) => `const ${key} = JSON.parse('${value}');`) - .join(''); - } - - return vm.run(`'use strict';${variables}${js}`); + return vm.run(`'use strict';${js}`); } diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index 54c9b64c..03377339 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -20,7 +20,7 @@ import { VariableExpressionDefinitionNode, } from '@superindustries/language'; -import { Sandbox } from '../../client/interpreter/Sandbox'; +import { evalScript } from '../../client/interpreter/Sandbox'; import { HttpClient } from '../http'; import { MapParameters, MapVisitor } from './interfaces'; @@ -136,9 +136,7 @@ export class MapInterpereter implements MapVisitor { node: JSExpressionNode, _parameters: MapParameters ): Promise { - const sandbox = new Sandbox(); - - return await sandbox.evalJS(node.expression, this.variables); + return await evalScript(node.expression, this.variables); } async visitMapDefinitionNode( From 56916e3cd6f138c2e43687889a72f0443ce72fb4 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Wed, 1 Jul 2020 16:15:23 +0200 Subject: [PATCH 03/15] feat: Add multistep operations --- src/internal/http.ts | 4 +- .../interpreter/map-interpreter.test.ts | 153 ++++++++++++++++++ src/internal/interpreter/map-interpreter.ts | 84 +++++++--- 3 files changed, 214 insertions(+), 27 deletions(-) diff --git a/src/internal/http.ts b/src/internal/http.ts index 5ef29634..06a09505 100644 --- a/src/internal/http.ts +++ b/src/internal/http.ts @@ -34,8 +34,8 @@ export const HttpClient = { }; if ( - parameters.method !== 'GET' && - parameters.method !== 'HEAD' && + parameters.method.toLowerCase() !== 'get' && + parameters.method.toLowerCase() !== 'head' && parameters.body ) { params.body = JSON.stringify(parameters.body); diff --git a/src/internal/interpreter/map-interpreter.test.ts b/src/internal/interpreter/map-interpreter.test.ts index c12a6231..43c3923c 100644 --- a/src/internal/interpreter/map-interpreter.test.ts +++ b/src/internal/interpreter/map-interpreter.test.ts @@ -31,6 +31,18 @@ const listener: RequestListener = ( res.end(); break; + case 'GET /first': + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify({ firstStep: { someVar: 12 } })); + res.end(); + break; + + case 'GET /second': + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify({ secondStep: 5 })); + res.end(); + break; + case 'POST /checkBody': req.on('data', data => (buffer += decoder.write(data))); @@ -657,4 +669,145 @@ describe('MapInterpreter', () => { }, }); }); + + it('should run multi step operation', async () => { + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'multistep', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'firstStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `http://localhost:${port}/first`, + method: 'get', + requestDefinition: { + queryParametersDefinition: [], + security: 'other', + headers: [], + body: [], + }, + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en_US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'someVariable', + right: { + kind: 'JSExpression', + expression: 'response.firstStep.someVar', + }, + }, + ], + }, + }, + }, + }, + }, + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'secondStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `http://localhost:${port}/second`, + method: 'get', + requestDefinition: { + queryParametersDefinition: [], + security: 'other', + headers: [], + body: [], + }, + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en_US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'someOtherVariable', + right: { + kind: 'JSExpression', + expression: 'response.secondStep', + }, + }, + ], + }, + }, + }, + }, + }, + { + kind: 'StepDefinition', + condition: { + kind: 'JSExpression', + expression: `typeof someOtherVariable !== 'undefined' && someOtherVariable && someOtherVariable < 10 && typeof someVariable !== undefined && someVariable && someVariable > 10`, + }, + variableExpressionsDefinition: [], + stepName: 'thirdStep', + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'someVariable * someOtherVariable', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'multistep' } + ); + + expect(result).toEqual({ result: 12 * 5 }); + }); }); diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index 03377339..d27c4fe3 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -34,10 +34,14 @@ export class MapInterpereter implements MapVisitor { private operations: OperationDefinitionNode[] = []; - private scopedVariables: Record> = {}; + private operationScopedVariables: Record> = {}; private operationScope: string | undefined; + private mapScopedVariables: Record> = {}; + + private mapScope: string | undefined; + async visit(node: MapASTNode, parameters: MapParameters): Promise { switch (node.kind) { case 'EvalDefinition': @@ -143,22 +147,29 @@ export class MapInterpereter implements MapVisitor { node: MapDefinitionNode, parameters: MapParameters ): Promise { - const viableSteps = node.stepsDefinition.filter(async stepDefinition => { - return await this.visit(stepDefinition.condition, parameters); - }); + this.mapScope = node.mapName; - if (viableSteps.length < 1) { - throw new Error('No step satisfies condition!'); - } + let result: unknown; + for (const step of node.stepsDefinition) { + const condition = await this.visit(step.condition, parameters); - const variables = await this.processVariableExpressions( - node.variableExpressionsDefinition, - parameters - ); + if (condition) { + const variables = await this.processVariableExpressions( + node.variableExpressionsDefinition, + parameters + ); - this.variableStack.push(variables); - const result = await this.visit(viableSteps[0], parameters); - this.variableStack.pop(); + this.variableStack.push(variables); + const stepResult = await this.visit(step, parameters); + this.variableStack.pop(); + + if (stepResult) { + result = stepResult; + } + } + } + + this.mapScope = undefined; return result; } @@ -254,16 +265,29 @@ export class MapInterpereter implements MapVisitor { parameters ); } else if (node.setDefinition) { - if (!this.operationScope) { - throw new Error( - 'Something very wrong happened. How did you even get here?' - ); + if (this.operationScope) { + this.operationScopedVariables[this.operationScope] = { + ...(this.operationScopedVariables[this.operationScope] || {}), + ...(await this.processVariableExpressions( + node.setDefinition, + parameters + )), + }; + + return undefined; + } else if (this.mapScope) { + this.mapScopedVariables[this.mapScope] = { + ...(this.mapScopedVariables[this.mapScope] || {}), + ...(await this.processVariableExpressions( + node.setDefinition, + parameters + )), + }; + + return undefined; + } else { + throw new Error('Something went very wrong, this should not happen!'); } - this.scopedVariables[ - this.operationScope - ] = await this.processVariableExpressions(node.setDefinition, parameters); - - return undefined; } else if (node.resultDefinition) { return await this.processMapExpressions( node.resultDefinition, @@ -322,10 +346,20 @@ export class MapInterpereter implements MapVisitor { {} ); - if (this.operationScope && this.scopedVariables[this.operationScope]) { + if (this.mapScope && this.mapScopedVariables[this.mapScope]) { + variables = { + ...variables, + ...this.mapScopedVariables[this.mapScope], + }; + } + + if ( + this.operationScope && + this.operationScopedVariables[this.operationScope] + ) { variables = { ...variables, - ...this.scopedVariables[this.operationScope], + ...this.operationScopedVariables[this.operationScope], }; } From 8761900f6180152f565138a54eb70130db0e5df2 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Wed, 15 Jul 2020 13:37:50 +0200 Subject: [PATCH 04/15] feat: Improve HTTP client --- package.json | 6 +- src/client/interpreter/Sandbox.ts | 6 +- src/internal/http.test.ts | 64 +- src/internal/http.ts | 124 +- src/internal/interpreter/interfaces.ts | 14 +- .../interpreter/map-interpreter.test.ts | 1054 +++++++++++++++-- src/internal/interpreter/map-interpreter.ts | 99 +- src/module.d.ts | 1 + yarn.lock | 891 +++++++++++++- 9 files changed, 2048 insertions(+), 211 deletions(-) create mode 100644 src/module.d.ts diff --git a/package.json b/package.json index f95871ae..1baaecc6 100644 --- a/package.json +++ b/package.json @@ -39,14 +39,16 @@ "eslint-plugin-simple-import-sort": "^5.0.3", "jest": "^26.0.1", "microbundle": "^0.12.0", + "mockttp": "^0.20.3", "prettier": "^2.0.5", "rimraf": "^3.0.2", "ts-jest": "^26.1.0", - "typescript": "^3.9.2" + "typescript": "^3.9.6" }, "dependencies": { - "cross-fetch": "^3.0.5", "@superindustries/language": "^0.0.7", + "cross-fetch": "^3.0.5", + "isomorphic-form-data": "^2.0.0", "vm2": "^3.9.2" } } diff --git a/src/client/interpreter/Sandbox.ts b/src/client/interpreter/Sandbox.ts index 5b366e28..9f2aa263 100644 --- a/src/client/interpreter/Sandbox.ts +++ b/src/client/interpreter/Sandbox.ts @@ -1,10 +1,12 @@ import { VM } from 'vm2'; +import { Variables } from '../../internal/interpreter/map-interpreter'; + export const SCRIPT_TIMEOUT = 100; export function evalScript( js: string, - variableDefinitions?: Record + variableDefinitions?: Variables ): unknown { const vm = new VM({ sandbox: { @@ -26,7 +28,7 @@ export function evalScript( delete globalThis.require // Forbidden delete globalThis.process // Forbidden delete globalThis.console // Forbidden/useless - + delete globalThis.setTimeout delete globalThis.setInterval delete globalThis.setImmediate diff --git a/src/internal/http.test.ts b/src/internal/http.test.ts index e1dc4d9d..0533af07 100644 --- a/src/internal/http.test.ts +++ b/src/internal/http.test.ts @@ -1,62 +1,38 @@ -import { - createServer, - IncomingMessage, - RequestListener, - ServerResponse, -} from 'http'; +import { getLocal } from 'mockttp'; import { HttpClient } from './http'; -const port = Math.floor(Math.random() * 64511 + 1024); - -const listener: RequestListener = ( - req: IncomingMessage, - res: ServerResponse -) => { - switch (`${req.method} ${req.url}`) { - case 'GET /valid': - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify({ response: 'valid' })); - break; - - case 'GET /invalid': - res.writeHead(404); - res.write(JSON.stringify({ error: { message: 'Not found' } })); - break; - - default: - throw new Error( - `Invalid combination of url and method: ${req.url}, ${req.method}` - ); - } - res.end(); -}; - -const server = createServer(listener); +const mockServer = getLocal(); describe('HttpClient', () => { - beforeAll(() => { - server.listen(port); + beforeEach(async () => { + await mockServer.start(); }); - afterAll(() => { - server.close(); + afterEach(async () => { + await mockServer.stop(); }); it('gets basic response', async () => { - const response = await HttpClient.request( - `http://localhost:${port}/valid`, - { method: 'get' } - ); + await mockServer.get('/valid').thenJson(200, { response: 'valid' }); + const url = mockServer.urlFor('/valid'); + const response = await HttpClient.request(url, { + method: 'get', + accept: 'application/json', + }); expect(response.statusCode).toEqual(200); expect(response.body).toEqual({ response: 'valid' }); }); it('gets error response', async () => { - const response = await HttpClient.request( - `http://localhost:${port}/invalid`, - { method: 'get' } - ); + await mockServer + .get('/invalid') + .thenJson(404, { error: { message: 'Not found' } }); + const url = mockServer.urlFor('/invalid'); + const response = await HttpClient.request(url, { + method: 'get', + accept: 'application/json', + }); expect(response.statusCode).toEqual(404); expect(response.body).toEqual({ error: { message: 'Not found' } }); }); diff --git a/src/internal/http.ts b/src/internal/http.ts index 06a09505..5a42f3a6 100644 --- a/src/internal/http.ts +++ b/src/internal/http.ts @@ -1,10 +1,55 @@ -import fetch from 'cross-fetch'; +import 'isomorphic-form-data'; + +import fetch, { Headers } from 'cross-fetch'; export interface HttpResponse { statusCode: number; body: unknown; + headers: Record; } +const AUTH_HEADER_NAME = 'Authorization'; +const JSON_CONTENT = 'application/json'; +const URLENCODED_CONTENT = 'application/x-www-form-urlencoded'; +const FORMDATA_CONTENT = 'multipart/form-data'; + +const queryParameters = (parameters?: Record): string => { + if (parameters && Object.keys(parameters).length) { + return '?' + new URLSearchParams(parameters).toString(); + } + + return ''; +}; + +const basicAuth = (auth?: { username: string; password: string }): string => { + if (!auth || !auth.username || !auth.password) { + throw new Error('Missing credentials for Basic Auth!'); + } + + return ( + 'Basic ' + + Buffer.from(`${auth.username}:${auth.password}`).toString('base64') + ); +}; + +const bearerAuth = (auth?: { token: string }): string => { + if (!auth || !auth.token) { + throw new Error('Missing token for Bearer Auth!'); + } + + return `Bearer ${auth.token}`; +}; + +const formData = (data?: Record): FormData => { + const formData = new FormData(); + + if (data) { + Object.entries(data).forEach(([key, value]) => formData.append(key, value)); + } + + return formData; +}; + export const HttpClient = { request: async ( url: string, @@ -12,40 +57,79 @@ export const HttpClient = { method: string; headers?: Record; queryParameters?: Record; - body?: unknown; + body?: Record; + contentType?: string; + accept?: string; + security?: 'basic' | 'bearer' | 'other'; + basic?: { username: string; password: string }; + bearer?: { token: string }; + baseUrl?: string; } ): Promise => { - let query = ''; + const query = queryParameters(parameters.queryParameters); - if ( - parameters?.queryParameters && - Object.keys(parameters.queryParameters).length - ) { - query = - '?' + - Object.entries(parameters.queryParameters) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - } + const headers = new Headers(parameters?.headers); + headers.append('Accept', parameters.accept ?? '*/*'); const params: RequestInit = { - headers: { 'Content-Type': 'application/json', ...parameters?.headers }, + headers, method: parameters.method, }; if ( - parameters.method.toLowerCase() !== 'get' && - parameters.method.toLowerCase() !== 'head' && - parameters.body + parameters.body && + ['post', 'put', 'patch'].includes(parameters.method.toLowerCase()) ) { - params.body = JSON.stringify(parameters.body); + if (parameters.contentType === JSON_CONTENT) { + headers.append('Content-Type', JSON_CONTENT); + params.body = JSON.stringify(parameters.body); + } else if (parameters.contentType === URLENCODED_CONTENT) { + headers.append('Content-Type', URLENCODED_CONTENT); + params.body = new URLSearchParams(parameters.body); + } else if (parameters.contentType === FORMDATA_CONTENT) { + headers.append('Content-Type', FORMDATA_CONTENT); + params.body = formData(parameters.body); + } else { + throw new Error(`Unknown content type: ${parameters.contentType}`); + } + } + + if (parameters.security === 'basic') { + headers.append(AUTH_HEADER_NAME, basicAuth(parameters.basic)); + } else if (parameters.security === 'bearer') { + headers.append(AUTH_HEADER_NAME, bearerAuth(parameters.bearer)); + } + + const isRelative = /^\/[^\/]/.test(url); + + if (isRelative && !parameters.baseUrl) { + throw new Error('Relative URL specified, but base URL not provided!'); + } + + const urlPrefix = isRelative ? parameters.baseUrl : ''; + + const response = await fetch( + encodeURI(`${urlPrefix}${url}${query}`), + params + ); + + let body: unknown; + + if (parameters.accept === JSON_CONTENT) { + body = await response.json(); + } else { + body = await response.text(); } - const response = await fetch(encodeURI(`${url}${query}`), params); + const responseHeaders: Record = {}; + response.headers.forEach((key, value) => { + responseHeaders[key] = value; + }); return { statusCode: response.status, - body: await response.json(), + body, + headers: responseHeaders, }; }, }; diff --git a/src/internal/interpreter/interfaces.ts b/src/internal/interpreter/interfaces.ts index 04e28a68..b4409ffa 100644 --- a/src/internal/interpreter/interfaces.ts +++ b/src/internal/interpreter/interfaces.ts @@ -8,11 +8,11 @@ import { MapDocumentNode, MapExpressionDefinitionNode, MapNode, + MapProfileIdNode, NetworkOperationDefinitionNode, OperationCallDefinitionNode, OperationDefinitionNode, OutcomeDefinitionNode, - ProfileIdNode, ProviderNode, StepDefinitionNode, VariableExpressionDefinitionNode, @@ -20,6 +20,16 @@ import { export interface MapParameters { usecase?: string; + auth?: { + basic?: { + username: string; + password: string; + }; + bearer?: { + token: string; + }; + }; + baseUrl?: string; } export interface MapVisitor { @@ -76,7 +86,7 @@ export interface MapVisitor { parameters: MapParameters ): Promise | unknown; visitProfileIdNode( - node: ProfileIdNode, + node: MapProfileIdNode, parameters: MapParameters ): Promise | unknown; visitProviderNode( diff --git a/src/internal/interpreter/map-interpreter.test.ts b/src/internal/interpreter/map-interpreter.test.ts index 43c3923c..17b6ba4c 100644 --- a/src/internal/interpreter/map-interpreter.test.ts +++ b/src/internal/interpreter/map-interpreter.test.ts @@ -1,87 +1,20 @@ import { MapASTNode } from '@superindustries/language'; -import { - createServer, - IncomingMessage, - RequestListener, - ServerResponse, -} from 'http'; -import { StringDecoder } from 'string_decoder'; +import { getLocal } from 'mockttp'; import { MapInterpereter } from './map-interpreter'; -const port = Math.floor(Math.random() * 64511 + 1024); - -const listener: RequestListener = ( - req: IncomingMessage, - res: ServerResponse -) => { - const decoder = new StringDecoder('utf-8'); - let buffer = ''; - - switch (`${req.method} ${req.url}`) { - case 'GET /twelve': - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify({ data: 12 })); - res.end(); - break; - - case 'GET /twelve?page=2': - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify({ data: 144 })); - res.end(); - break; - - case 'GET /first': - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify({ firstStep: { someVar: 12 } })); - res.end(); - break; - - case 'GET /second': - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify({ secondStep: 5 })); - res.end(); - break; - - case 'POST /checkBody': - req.on('data', data => (buffer += decoder.write(data))); - - req.on('end', () => { - buffer += decoder.end(); - const body = JSON.parse(buffer); - res.writeHead(201, { 'Content-Type': 'application/json' }); - res.write( - JSON.stringify({ - bodyOk: body.anArray.length === 3, - headerOk: req.headers['someheader'] === 'hello', - }) - ); - res.end(); - }); - break; - - default: - throw new Error( - `Invalid combination of url and method: ${req.url}, ${req.method}` - ); - } -}; - -const server = createServer(listener); +const mockServer = getLocal(); describe('MapInterpreter', () => { let interpreter: MapInterpereter; - beforeAll(() => { - server.listen(port); - }); - - afterAll(() => { - server.close(); + beforeEach(async () => { + interpreter = new MapInterpereter(); + await mockServer.start(); }); - beforeEach(() => { - interpreter = new MapInterpereter(); + afterEach(async () => { + await mockServer.stop(); }); it('should fail with invalid AST', async () => { @@ -154,6 +87,119 @@ describe('MapInterpreter', () => { expect(result).toEqual({ result: 12 }); }); + it('should fail on undefined usecase', async () => { + await expect( + async () => + await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: '12', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'nonexistent' } + ) + ).rejects.toThrow('Usecase not found.'); + }); + + // This should not happen in practice, as the AST will be validated beforehand + it('should fail when none of result/return/set are defined', async () => { + await expect( + async () => + await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ) + ).rejects.toThrow('Something went very wrong, this should not happen!'); + }); + it('should execute Eval definition with variables', async () => { const result = await interpreter.visit( { @@ -398,7 +444,107 @@ describe('MapInterpreter', () => { expect(result).toEqual({ result: 12 }); }); + it('should throw when trying to run undefined operation', async () => { + await expect( + async () => + await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'OperationCallDefinition', + arguments: [], + operationName: 'my beloved operation', + successOutcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'variableFromOperation', + }, + }, + ], + }, + }, + }, + ], + }, + { + kind: 'OperationDefinition', + operationName: 'my not-so-beloved operation', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'step', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'variableFromOperation', + right: { + kind: 'JSExpression', + expression: '12', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ) + ).rejects.toThrow('Operation my beloved operation not found'); + }); + it('should call an API', async () => { + await mockServer.get('/twelve').thenJson(200, { data: 12 }); + const url = mockServer.urlFor('/twelve'); const result = await interpreter.visit( { kind: 'MapDocument', @@ -436,12 +582,12 @@ describe('MapInterpreter', () => { definition: { kind: 'HTTPOperationDefinition', variableExpressionsDefinition: [], - url: `http://localhost:${port}/twelve`, + url, method: 'GET', responseDefinition: { statusCode: 200, contentType: 'application/json', - contentLanguage: 'en_US', + contentLanguage: 'en-US', outcomeDefinition: { kind: 'OutcomeDefinition', resultDefinition: [ @@ -450,13 +596,14 @@ describe('MapInterpreter', () => { left: 'result', right: { kind: 'JSExpression', - expression: 'response.data', + expression: 'body.data', }, }, ], }, }, requestDefinition: { + contentType: 'application/json', body: [], headers: [], security: 'other', @@ -475,7 +622,172 @@ describe('MapInterpreter', () => { expect(result).toEqual({ result: 12 }); }); + it('should call an API with relative URL', async () => { + await mockServer.get('/twelve').thenJson(200, { data: 12 }); + const baseUrl = mockServer.urlFor('/twelve').replace('/twelve', ''); + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/twelve', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase', baseUrl } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should throw when calling an API with relative URL but not providing baseUrl', async () => { + await mockServer.get('/twelve').thenJson(200, { data: 12 }); + await expect( + async () => + await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/twelve', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ) + ).rejects.toThrow('Relative URL specified, but base URL not provided!'); + }); + it('should call an API with parameters', async () => { + await mockServer + .get('/twelve') + .withQuery({ page: 2 }) + .thenJson(200, { data: 144 }); + const url = mockServer.urlFor('/twelve'); const result = await interpreter.visit( { kind: 'MapDocument', @@ -522,12 +834,12 @@ describe('MapInterpreter', () => { definition: { kind: 'HTTPOperationDefinition', variableExpressionsDefinition: [], - url: `http://localhost:${port}/twelve`, + url, method: 'GET', responseDefinition: { statusCode: 200, contentType: 'application/json', - contentLanguage: 'en_US', + contentLanguage: 'en-US', outcomeDefinition: { kind: 'OutcomeDefinition', resultDefinition: [ @@ -536,13 +848,14 @@ describe('MapInterpreter', () => { left: 'result', right: { kind: 'JSExpression', - expression: 'response.data', + expression: 'body.data', }, }, ], }, }, requestDefinition: { + contentType: 'application/json', body: [], headers: [], security: 'other', @@ -571,6 +884,12 @@ describe('MapInterpreter', () => { }); it('should call an API with parameters and POST request', async () => { + await mockServer + .post('/checkBody') + .withJsonBody({ anArray: [1, 2, 3] }) + .withHeaders({ someheader: 'hello' }) + .thenJson(201, { bodyOk: true, headerOk: true }); + const url = mockServer.urlFor('/checkBody'); const result = await interpreter.visit( { kind: 'MapDocument', @@ -608,12 +927,12 @@ describe('MapInterpreter', () => { definition: { kind: 'HTTPOperationDefinition', variableExpressionsDefinition: [], - url: `http://localhost:${port}/checkBody`, + url, method: 'POST', responseDefinition: { statusCode: 201, contentType: 'application/json', - contentLanguage: 'en_US', + contentLanguage: 'en-US', outcomeDefinition: { kind: 'OutcomeDefinition', resultDefinition: [ @@ -622,13 +941,14 @@ describe('MapInterpreter', () => { left: 'result', right: { kind: 'JSExpression', - expression: 'response', + expression: 'body', }, }, ], }, }, requestDefinition: { + contentType: 'application/json', body: [ { kind: 'MapExpressionsDefinition', @@ -671,6 +991,12 @@ describe('MapInterpreter', () => { }); it('should run multi step operation', async () => { + await mockServer + .get('/first') + .thenJson(200, { firstStep: { someVar: 12 } }); + await mockServer.get('/second').thenJson(200, { secondStep: 5 }); + const url1 = mockServer.urlFor('/first'); + const url2 = mockServer.urlFor('/second'); const result = await interpreter.visit( { kind: 'MapDocument', @@ -705,9 +1031,10 @@ describe('MapInterpreter', () => { definition: { kind: 'HTTPOperationDefinition', variableExpressionsDefinition: [], - url: `http://localhost:${port}/first`, + url: url1, method: 'get', requestDefinition: { + contentType: 'application/json', queryParametersDefinition: [], security: 'other', headers: [], @@ -716,7 +1043,7 @@ describe('MapInterpreter', () => { responseDefinition: { statusCode: 200, contentType: 'application/json', - contentLanguage: 'en_US', + contentLanguage: 'en-US', outcomeDefinition: { kind: 'OutcomeDefinition', setDefinition: [ @@ -725,7 +1052,7 @@ describe('MapInterpreter', () => { left: 'someVariable', right: { kind: 'JSExpression', - expression: 'response.firstStep.someVar', + expression: 'body.firstStep.someVar', }, }, ], @@ -747,9 +1074,10 @@ describe('MapInterpreter', () => { definition: { kind: 'HTTPOperationDefinition', variableExpressionsDefinition: [], - url: `http://localhost:${port}/second`, + url: url2, method: 'get', requestDefinition: { + contentType: 'application/json', queryParametersDefinition: [], security: 'other', headers: [], @@ -758,7 +1086,7 @@ describe('MapInterpreter', () => { responseDefinition: { statusCode: 200, contentType: 'application/json', - contentLanguage: 'en_US', + contentLanguage: 'en-US', outcomeDefinition: { kind: 'OutcomeDefinition', setDefinition: [ @@ -767,7 +1095,7 @@ describe('MapInterpreter', () => { left: 'someOtherVariable', right: { kind: 'JSExpression', - expression: 'response.secondStep', + expression: 'body.secondStep', }, }, ], @@ -810,4 +1138,552 @@ describe('MapInterpreter', () => { expect(result).toEqual({ result: 12 * 5 }); }); + + it('should call an API with Basic auth', async () => { + await mockServer + .get('/basic') + .withHeaders({ Authorization: 'Basic bmFtZTpwYXNzd29yZA==' }) + .thenJson(200, { data: 12 }); + const url = mockServer.urlFor('/basic'); + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'basic', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { + usecase: 'testCase', + auth: { basic: { username: 'name', password: 'password' } }, + } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should call an API with Bearer auth', async () => { + await mockServer + .get('/bearer') + .withHeaders({ Authorization: 'Bearer SuperSecret' }) + .thenJson(200, { data: 12 }); + const url = mockServer.urlFor('/bearer'); + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'bearer', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { + usecase: 'testCase', + auth: { bearer: { token: 'SuperSecret' } }, + } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should call an API with multipart/form-data body', async () => { + await mockServer.post('/formdata').thenCallback(request => { + if ( + request.body.text && + request.body.text.includes('formData') && + request.body.text.includes('myFormData') && + request.body.text.includes('is') && + request.body.text.includes('present') + ) { + return { + json: { data: 12 }, + status: 201, + }; + } + + return { json: { failed: true }, statusCode: 400 }; + }); + const url = mockServer.urlFor('/formdata'); + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'POST', + responseDefinition: { + statusCode: 201, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'multipart/form-data', + body: [ + { + kind: 'MapExpressionsDefinition', + left: 'formData', + right: { + kind: 'JSExpression', + expression: '"myFormData"', + }, + }, + { + kind: 'MapExpressionsDefinition', + left: 'is', + right: { + kind: 'JSExpression', + expression: '"present"', + }, + }, + ], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ result: 12 }); + }); + + it('should throw on an API with Basic auth, but without credentials', async () => { + await expect( + async () => + await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/unimportant', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'basic', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { + usecase: 'testCase', + } + ) + ).rejects.toThrow(); + }); + + it('should throw on an API with Bearer auth, but without credentials', async () => { + await expect( + async () => + await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/unimportant', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'bearer', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { + usecase: 'testCase', + } + ) + ).rejects.toThrow(); + }); + + it('should call an API with application/x-www-form-urlencoded', async () => { + await mockServer + .post('/urlencoded') + .withForm({ form: 'is', o: 'k' }) + .thenJson(201, { data: 12 }); + const url = mockServer.urlFor('/urlencoded'); + const result = await interpreter.visit( + { + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'POST', + responseDefinition: { + statusCode: 201, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/x-www-form-urlencoded', + body: [ + { + kind: 'MapExpressionsDefinition', + left: 'form', + right: { + kind: 'JSExpression', + expression: '"is"', + }, + }, + { + kind: 'MapExpressionsDefinition', + left: 'o', + right: { + kind: 'JSExpression', + expression: '"k"', + }, + }, + ], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }, + { usecase: 'testCase' } + ); + + expect(result).toEqual({ + result: 12, + }); + }); }); diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index d27c4fe3..c5b773f7 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -10,11 +10,11 @@ import { MapDocumentNode, MapExpressionDefinitionNode, MapNode, + MapProfileIdNode, NetworkOperationDefinitionNode, OperationCallDefinitionNode, OperationDefinitionNode, OutcomeDefinitionNode, - ProfileIdNode, ProviderNode, StepDefinitionNode, VariableExpressionDefinitionNode, @@ -24,21 +24,25 @@ import { evalScript } from '../../client/interpreter/Sandbox'; import { HttpClient } from '../http'; import { MapParameters, MapVisitor } from './interfaces'; -function assertUnreachable(_node: never): never; +function assertUnreachable(node: never): never; function assertUnreachable(node: MapASTNode): never { throw new Error(`Invalid Node kind: ${node.kind}`); } +export type Variables = { + [key: string]: string | Variables; +}; + export class MapInterpereter implements MapVisitor { - private variableStack: Record[] = []; + private variableStack: Variables[] = []; private operations: OperationDefinitionNode[] = []; - private operationScopedVariables: Record> = {}; + private operationScopedVariables: Record = {}; private operationScope: string | undefined; - private mapScopedVariables: Record> = {}; + private mapScopedVariables: Record = {}; private mapScope: string | undefined; @@ -119,9 +123,18 @@ export class MapInterpereter implements MapVisitor { method: node.method, body, headers, + contentType: node.requestDefinition.contentType, + accept: node.responseDefinition.contentType, + security: node.requestDefinition.security, + basic: parameters.auth?.basic, + bearer: parameters.auth?.bearer, + baseUrl: parameters.baseUrl, }); - this.variableStack.push({ response: response.body as string }); + this.variableStack.push({ + body: response.body as string, + headers: response.headers, + }); return await this.visit( node.responseDefinition.outcomeDefinition, @@ -185,7 +198,7 @@ export class MapInterpereter implements MapVisitor { .find(definition => definition.usecaseName === parameters.usecase); if (!operation) { - throw new Error('Operation not found'); + throw new Error('Usecase not found.'); } return await this.visit(operation, parameters); @@ -226,9 +239,10 @@ export class MapInterpereter implements MapVisitor { throw new Error(`Operation ${node.operationName} not found!`); } - this.operationScope = operation.operationName; let result = await this.visit(operation, parameters); + this.operationScope = operation.operationName; + if (!result) { result = await this.visit(node.successOutcomeDefinition, parameters); } @@ -242,15 +256,29 @@ export class MapInterpereter implements MapVisitor { node: OperationDefinitionNode, parameters: MapParameters ): Promise { - const viableSteps = node.stepsDefinition.filter(async stepDefinition => { - return await this.visit(stepDefinition.condition, parameters); - }); + this.operationScope = node.operationName; + + let result: unknown; + for (const step of node.stepsDefinition) { + const condition = await this.visit(step.condition, parameters); - if (viableSteps.length < 1) { - throw new Error('No step satisfies condition!'); + if (condition) { + const variables = await this.processVariableExpressions( + node.variableExpressionsDefinition, + parameters + ); + + this.variableStack.push(variables); + const stepResult = await this.visit(step, parameters); + this.variableStack.pop(); + + if (stepResult) { + result = stepResult; + } + } } - const result = await this.visit(viableSteps[0], parameters); + this.operationScope = undefined; return result; } @@ -285,21 +313,18 @@ export class MapInterpereter implements MapVisitor { }; return undefined; - } else { - throw new Error('Something went very wrong, this should not happen!'); } } else if (node.resultDefinition) { return await this.processMapExpressions( node.resultDefinition, parameters ); - } else { - throw new Error('Something went very wrong, this should not happen!'); } + throw new Error('Something went very wrong, this should not happen!'); } visitProfileIdNode( - _node: ProfileIdNode, + _node: MapProfileIdNode, _parameters: MapParameters ): Promise | unknown { throw new Error('Method not implemented.'); @@ -337,7 +362,7 @@ export class MapInterpereter implements MapVisitor { }; } - private get variables(): Record { + private get variables(): Variables { let variables = this.variableStack.reduce( (acc, variableDefinition) => ({ ...acc, @@ -370,25 +395,31 @@ export class MapInterpereter implements MapVisitor { expressions: VariableExpressionDefinitionNode[], parameters: MapParameters ): Promise> { - return expressions.reduce( - async (acc, expression) => ({ - ...acc, - ...((await this.visit(expression, parameters)) as {}), - }), - Promise.resolve({}) - ); + let variables: Record = {}; + for (const expression of expressions) { + const result = (await this.visit(expression, parameters)) as Record< + string, + string + >; + variables = { ...variables, ...result }; + } + + return variables; } private async processMapExpressions( expressions: MapExpressionDefinitionNode[], parameters: MapParameters ): Promise> { - return expressions.reduce( - async (acc, expression) => ({ - ...acc, - ...((await this.visit(expression, parameters)) as {}), - }), - Promise.resolve({}) - ); + let variables: Record = {}; + for (const expression of expressions) { + const result = (await this.visit(expression, parameters)) as Record< + string, + string + >; + variables = { ...variables, ...result }; + } + + return variables; } } diff --git a/src/module.d.ts b/src/module.d.ts new file mode 100644 index 00000000..b205bf42 --- /dev/null +++ b/src/module.d.ts @@ -0,0 +1 @@ +declare module 'isomorphic-form-data'; diff --git a/yarn.lock b/yarn.lock index fd469a8a..a492e9a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -854,6 +854,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@httptoolkit/httpolyglot@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@httptoolkit/httpolyglot/-/httpolyglot-0.2.0.tgz#42833eb2bd0627c4cf0ab816da8e81318b015f71" + integrity sha512-oTOzauUIkt0UA/mPsuKakhr1HT7kKDJrhpiBR8ApUcLSdWVSincQ1lSi89gRczxBiaimhOnEtzZ8KGBMNDxuvQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -1144,11 +1149,33 @@ dependencies: "@babel/types" "^7.3.0" +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.1": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" + integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== + dependencies: + "@types/express" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1164,6 +1191,25 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/express-serve-static-core@*": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz#b8f7b714138536742da222839892e203df569d1c" + integrity sha512-1SJZ+R3Q/7mLkOD9ewCBDYD2k0WyZQtWYqF/2VvoNN2/uhI49J9CDN4OAm+wGMA0DbArA4ef27xl4+JwMtGggw== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.0.33": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59" + integrity sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.2": version "4.1.3" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" @@ -1209,11 +1255,28 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/mime@*": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" + integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q== + +"@types/node-forge@^0.9.1": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.4.tgz#3a81edafed5022eba8e63338ef4b04def04c61f1" + integrity sha512-uFhaKXdhhrLNzfNhXbXJqDwF3jXMzN9qfkdW+IAMnAfwqNZhBcE/cciMITLT0Sg6ls6JYHo3xVWNXAG1g9tm8A== + dependencies: + "@types/node" "*" + "@types/node@*": version "14.0.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c" integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA== +"@types/node@^10.12.9": + version "10.17.27" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.27.tgz#391cb391c75646c8ad2a7b6ed3bbcee52d1bdf19" + integrity sha512-J0oqm9ZfAXaPdwNXMMgAhylw5fhmXkToJd06vuDUSAgEDZ/n/69/69UmyBZbc+zT34UnShuDSBqvim3SPnozJg== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -1234,6 +1297,16 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/qs@*": + version "6.9.3" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.3.tgz#b755a0934564a200d3efdf88546ec93c369abd03" + integrity sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -1241,6 +1314,14 @@ dependencies: "@types/node" "*" +"@types/serve-static@*": + version "1.13.4" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.4.tgz#6662a93583e5a6cabca1b23592eb91e12fa80e7c" + integrity sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1301,11 +1382,26 @@ semver "^7.3.2" tsutils "^3.17.1" +"@wry/equality@^0.1.2": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790" + integrity sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA== + dependencies: + tslib "^1.9.3" + abab@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -1402,6 +1498,62 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +apollo-cache-control@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" + integrity sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA== + dependencies: + graphql-extensions "^0.0.x" + +apollo-link@^1.2.14: + version "1.2.14" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9" + integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.21" + +apollo-server-core@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592" + integrity sha512-BP1Vh39krgEjkQxbjTdBURUjLHbFq1zeOChDJgaRsMxGtlhzuLWwwC6lLdPatN8jEPbeHq8Tndp9QZ3iQZOKKA== + dependencies: + apollo-cache-control "^0.1.0" + apollo-tracing "^0.1.0" + graphql-extensions "^0.0.x" + +apollo-server-express@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-1.4.0.tgz#7d7c58d6d6f9892b83fe575669093bb66738b125" + integrity sha512-zkH00nxhLnJfO0HgnNPBTfZw8qI5ILaPZ5TecMCI9+Y9Ssr2b0bFr9pBRsXy9eudPhI+/O4yqegSUsnLdF/CPw== + dependencies: + apollo-server-core "^1.4.0" + apollo-server-module-graphiql "^1.4.0" + +apollo-server-module-graphiql@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec" + integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA== + +apollo-tracing@^0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7" + integrity sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ== + dependencies: + graphql-extensions "~0.0.9" + +apollo-utilities@^1.0.1, apollo-utilities@^1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf" + integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig== + dependencies: + "@wry/equality" "^0.1.2" + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.4.0" + tslib "^1.10.0" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1424,6 +1576,11 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + array-includes@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -1468,6 +1625,18 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1592,11 +1761,26 @@ babel-preset-jest@^26.0.0: babel-plugin-jest-hoist "^26.0.0" babel-preset-current-node-syntax "^0.1.2" +backo2@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-arraybuffer@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + +base64-js@^1.1.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1622,6 +1806,22 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +body-parser@1.19.0, body-parser@^1.15.2: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -1665,6 +1865,13 @@ brotli-size@^4.0.0: dependencies: duplexer "0.1.1" +brotli@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/brotli/-/brotli-1.3.2.tgz#525a9cad4fcba96475d7d388f6aecb13eed52f46" + integrity sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y= + dependencies: + base64-js "^1.1.2" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -1704,6 +1911,11 @@ builtin-modules@^3.1.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1941,6 +2153,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +common-tags@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -1968,6 +2185,18 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -1975,6 +2204,16 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -1988,11 +2227,29 @@ core-js-compat@^3.6.2: browserslist "^4.8.5" semver "7.0.0" -core-util-is@1.0.2: +core-js@^2.5.3: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cors-gate@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cors-gate/-/cors-gate-1.1.3.tgz#4ff964e958a94f78da2029f0f95842410d812d19" + integrity sha512-RFqvbbpj02lqKDhqasBEkgzmT3RseCH3DKy5sT2W9S1mhctABKQP3ktKcnKN0h8t4pJ2SneI3hPl3TGNi/VmZA== + +cors@^2.8.4: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -2224,13 +2481,20 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" +debug@^3.1.1: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -2297,6 +2561,21 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +deprecated-decorator@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" + integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -2372,6 +2651,16 @@ duplexer@0.1.1, duplexer@^0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= +duplexify@^3.5.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -2380,6 +2669,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + electron-to-chromium@^1.3.413: version "1.3.441" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.441.tgz#094f71b992dca5bc96b798cfbaf37dc76302015a" @@ -2400,7 +2694,19 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -end-of-stream@^1.1.0: +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +encoding@^0.1.11: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2450,6 +2756,11 @@ es6-promisify@^6.0.1: resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621" integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -2651,6 +2962,16 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + eventemitter3@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" @@ -2719,6 +3040,42 @@ expect@^26.0.1: jest-message-util "^26.0.1" jest-regex-util "^26.0.0" +express@^4.14.0: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -2799,6 +3156,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-ponyfill@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" + integrity sha1-rjzl9zLGReq4fkroeTQUcJsjmJM= + dependencies: + node-fetch "~1.7.1" + figures@^1.0.1: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2843,6 +3207,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-cache-dir@^3.0.0: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" @@ -2891,6 +3268,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^2.3.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -2900,6 +3286,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -2907,6 +3298,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + fs-extra@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -3030,6 +3426,39 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: + version "0.0.10" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d" + integrity sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA== + dependencies: + core-js "^2.5.3" + source-map-support "^0.5.1" + +graphql-subscriptions@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11" + integrity sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA== + dependencies: + iterall "^1.2.1" + +graphql-tools@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.8.tgz#e7fb9f0d43408fb0878ba66b522ce871bafe9d30" + integrity sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg== + dependencies: + apollo-link "^1.2.14" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + iterall "^1.1.3" + uuid "^3.1.0" + +graphql@^14.0.2: + version "14.7.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72" + integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA== + dependencies: + iterall "^1.2.2" + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -3165,6 +3594,28 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -3186,6 +3637,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" + integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -3266,11 +3724,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + inquirer@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" @@ -3302,6 +3765,11 @@ ip-regex@^2.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" @@ -3468,6 +3936,11 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -3499,7 +3972,7 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-stream@^1.1.0: +is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -3545,7 +4018,7 @@ is-wsl@^2.1.1: dependencies: is-docker "^2.0.0" -isarray@1.0.0, isarray@^1.0.0: +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -3567,6 +4040,13 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-form-data@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz#9f6adf1c4c61ae3aefd8f110ab60fb9b143d6cec" + integrity sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg== + dependencies: + form-data "^2.3.2" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -3613,6 +4093,11 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" + integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== + jest-changed-files@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.0.1.tgz#1334630c6a1ad75784120f39c3aa9278e59f349f" @@ -4233,6 +4718,11 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= +lodash@^4.16.4: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -4310,11 +4800,26 @@ mdn-data@2.0.6: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + microbundle@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/microbundle/-/microbundle-0.12.0.tgz#d3d531c4d7553ea2f38688e3076f8e4d70adcc65" @@ -4390,13 +4895,18 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: mime-db "1.44.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4434,6 +4944,41 @@ mkdirp@^0.5.1, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" +mockttp@^0.20.3: + version "0.20.3" + resolved "https://registry.yarnpkg.com/mockttp/-/mockttp-0.20.3.tgz#916ad20187c47f3a21813319600a519ebb2481f0" + integrity sha512-aWy0nwbl3KDl7IRvKb9Jsn7AteTcc0DStGHm4KYX+Xf8+1hrxXRNPR7EJgJOJUnD/b8N38MlGebrZATeeOvWEg== + dependencies: + "@httptoolkit/httpolyglot" "^0.2.0" + "@types/cors" "^2.8.1" + "@types/express" "^4.0.33" + "@types/node" "^10.12.9" + "@types/node-forge" "^0.9.1" + apollo-server-express "^1.1.0" + base64-arraybuffer "^0.1.5" + body-parser "^1.15.2" + brotli "^1.3.2" + common-tags "^1.8.0" + cors "^2.8.4" + cors-gate "^1.1.3" + express "^4.14.0" + fetch-ponyfill "^4.1.0" + graphql "^14.0.2" + graphql-subscriptions "^1.1.0" + graphql-tools "^4.0.5" + lodash "^4.16.4" + native-duplexpair "^1.0.0" + node-forge "^0.9.0" + normalize-url "^1.9.1" + performance-now "^2.1.0" + portfinder "^1.0.23" + subscriptions-transport-ws "^0.9.4" + typed-error "^3.0.2" + universal-websocket-client "^1.0.2" + uuid "^3.1.0" + websocket-stream "^5.1.2" + ws "^5.2.0" + module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" @@ -4449,6 +4994,11 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4476,11 +5026,21 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +native-duplexpair@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz#7899078e64bf3c8a3d732601b3d40ff05db58fa0" + integrity sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A= + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4491,6 +5051,19 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-fetch@~1.7.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-forge@^0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4545,6 +5118,16 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= +normalize-url@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + normalize-url@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" @@ -4591,7 +5174,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.0: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -4657,6 +5240,13 @@ object.values@^1.1.0: function-bind "^1.1.1" has "^1.0.3" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4800,6 +5390,11 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -4835,6 +5430,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -4900,6 +5500,15 @@ pkg-up@^2.0.0: dependencies: find-up "^2.1.0" +portfinder@^1.0.23: + version "1.0.26" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" + integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -5267,6 +5876,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + prettier@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" @@ -5309,6 +5923,11 @@ private@^0.1.8: resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -5327,6 +5946,14 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -5350,11 +5977,39 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^16.12.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -5396,6 +6051,19 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +readable-stream@^2.0.0, readable-stream@^2.3.3: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -5715,16 +6383,16 @@ sade@^1.7.3: dependencies: mri "^1.1.0" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-identifier@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.1.tgz#b6516bf72594f03142b5f914f4c01842ccb1b678" @@ -5737,7 +6405,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -5789,11 +6457,40 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -5809,6 +6506,11 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -5899,6 +6601,13 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -5910,7 +6619,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6, source-map-support@~0.5.12: +source-map-support@^0.5.1, source-map-support@^0.5.6, source-map-support@~0.5.12: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -6016,11 +6725,26 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + string-hash@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" @@ -6086,6 +6810,13 @@ string.prototype.trimstart@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -6146,6 +6877,17 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +subscriptions-transport-ws@^0.9.4: + version "0.9.16" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" + integrity sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -6206,6 +6948,11 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" +symbol-observable@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -6324,6 +7071,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -6348,6 +7100,13 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +ts-invariant@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" + ts-jest@^26.1.0: version "26.1.0" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.0.tgz#e9070fc97b3ea5557a48b67c631c74eb35e15417" @@ -6379,7 +7138,7 @@ tslib@1.10.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== @@ -6437,6 +7196,19 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-error@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/typed-error/-/typed-error-3.2.0.tgz#ffa498688c458b7437e83c3c972d2b7b5b0e21ae" + integrity sha512-n0NojMTp7jD2MMgJxtjzS1it/sKIlDfQwqOECSPAGwsIU2jns3G0R6alnakRelQzxz7t8PhjYrlqYoQKUVGOsQ== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6444,11 +7216,21 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^3.8.3, typescript@^3.9.2: +typescript@^3.8.3: version "3.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== +typescript@^3.9.6: + version "3.9.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" + integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== + +ultron@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" + integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -6492,11 +7274,23 @@ uniqs@^2.0.0: resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= +universal-websocket-client@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/universal-websocket-client/-/universal-websocket-client-1.0.2.tgz#9942307a9d418bb5defd33594048c7e367448b44" + integrity sha512-Pi6BdJtEAISb77GTbOLBLIWdYGezXgnJejrVBYKXxzNTsLcjJS+mWIJ2BRZElSlOG/wc7+yfOe5y30bzTu3Qqg== + dependencies: + ws "^3.3.3" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -6527,6 +7321,11 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + util.promisify@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" @@ -6537,7 +7336,12 @@ util.promisify@~1.0.0: has-symbols "^1.0.1" object.getownpropertydescriptors "^2.1.0" -uuid@^3.3.2: +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.1.0, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -6569,6 +7373,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -6624,6 +7433,18 @@ webidl-conversions@^6.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +websocket-stream@^5.1.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/websocket-stream/-/websocket-stream-5.5.2.tgz#49d87083d96839f0648f5513bbddd581f496b8a2" + integrity sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ== + dependencies: + duplexify "^3.5.1" + inherits "^2.0.1" + readable-stream "^2.3.3" + safe-buffer "^5.1.2" + ws "^3.2.0" + xtend "^4.0.0" + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -6700,6 +7521,22 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@^3.2.0, ws@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" + integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== + dependencies: + async-limiter "~1.0.0" + safe-buffer "~5.1.0" + ultron "~1.1.0" + +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + ws@^7.2.3: version "7.3.0" resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" @@ -6715,6 +7552,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -6749,3 +7591,16 @@ yargs@^15.3.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.1" + +zen-observable-ts@^0.8.21: + version "0.8.21" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d" + integrity sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + +zen-observable@^0.8.0: + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== From 9ac8e1064702d1669b16b720093a45a051b069f5 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Wed, 15 Jul 2020 14:20:16 +0200 Subject: [PATCH 05/15] chore: Fix lint error --- src/internal/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/http.ts b/src/internal/http.ts index 5a42f3a6..adcc0f52 100644 --- a/src/internal/http.ts +++ b/src/internal/http.ts @@ -100,7 +100,7 @@ export const HttpClient = { headers.append(AUTH_HEADER_NAME, bearerAuth(parameters.bearer)); } - const isRelative = /^\/[^\/]/.test(url); + const isRelative = /^\/[^/]/.test(url); if (isRelative && !parameters.baseUrl) { throw new Error('Relative URL specified, but base URL not provided!'); From 151d697ee6f33c5bba97b1ad194905282443ef1b Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Mon, 20 Jul 2020 17:16:24 +0200 Subject: [PATCH 06/15] feat(map-interpreter): Add input and path parameters --- src/client/interpreter/Sandbox.ts | 2 +- src/internal/http.ts | 70 +- src/internal/interpreter/interfaces.ts | 85 +- .../interpreter/map-interpreter.test.ts | 2980 +++++++++-------- src/internal/interpreter/map-interpreter.ts | 223 +- 5 files changed, 1793 insertions(+), 1567 deletions(-) diff --git a/src/client/interpreter/Sandbox.ts b/src/client/interpreter/Sandbox.ts index 9f2aa263..7fe56466 100644 --- a/src/client/interpreter/Sandbox.ts +++ b/src/client/interpreter/Sandbox.ts @@ -1,6 +1,6 @@ import { VM } from 'vm2'; -import { Variables } from '../../internal/interpreter/map-interpreter'; +import { Variables } from '../../internal/interpreter/interfaces'; export const SCRIPT_TIMEOUT = 100; diff --git a/src/internal/http.ts b/src/internal/http.ts index adcc0f52..c1db38c5 100644 --- a/src/internal/http.ts +++ b/src/internal/http.ts @@ -2,6 +2,9 @@ import 'isomorphic-form-data'; import fetch, { Headers } from 'cross-fetch'; +import { evalScript } from '../client/interpreter/Sandbox'; +import { Variables } from './interpreter/interfaces'; + export interface HttpResponse { statusCode: number; body: unknown; @@ -50,6 +53,54 @@ const formData = (data?: Record): FormData => { return formData; }; +const createUrl = async ( + inputUrl: string, + parameters: { + baseUrl?: string; + pathParameters?: Variables; + queryParameters?: Record; + } +): Promise => { + const query = queryParameters(parameters.queryParameters); + const isRelative = /^\/[^/]/.test(inputUrl); + + if (isRelative && !parameters.baseUrl) { + throw new Error('Relative URL specified, but base URL not provided!'); + } + + let url = isRelative ? `${parameters.baseUrl}${inputUrl}` : inputUrl; + + if (parameters.pathParameters) { + const pathParameters = Object.keys(parameters.pathParameters); + const replacements: string[] = []; + + const regex = RegExp('{(.*?)}', 'g'); + let replacement: RegExpExecArray | null; + while ((replacement = regex.exec(url)) !== null) { + replacements.push(replacement[1]); + } + + const missingKeys = replacements.filter( + key => !pathParameters.includes(key) + ); + + if (missingKeys.length) { + throw new Error( + `Values for URL replacement keys not found: ${missingKeys.join(', ')}` + ); + } + + for (const param of pathParameters) { + url = url.replace( + `{${param}}`, + (await evalScript(param, parameters.pathParameters)) as string + ); + } + } + + return `${url}${query}`; +}; + export const HttpClient = { request: async ( url: string, @@ -64,10 +115,9 @@ export const HttpClient = { basic?: { username: string; password: string }; bearer?: { token: string }; baseUrl?: string; + pathParameters?: Variables; } ): Promise => { - const query = queryParameters(parameters.queryParameters); - const headers = new Headers(parameters?.headers); headers.append('Accept', parameters.accept ?? '*/*'); @@ -100,16 +150,14 @@ export const HttpClient = { headers.append(AUTH_HEADER_NAME, bearerAuth(parameters.bearer)); } - const isRelative = /^\/[^/]/.test(url); - - if (isRelative && !parameters.baseUrl) { - throw new Error('Relative URL specified, but base URL not provided!'); - } - - const urlPrefix = isRelative ? parameters.baseUrl : ''; - const response = await fetch( - encodeURI(`${urlPrefix}${url}${query}`), + encodeURI( + await createUrl(url, { + baseUrl: parameters.baseUrl, + pathParameters: parameters.pathParameters, + queryParameters: parameters.queryParameters, + }) + ), params ); diff --git a/src/internal/interpreter/interfaces.ts b/src/internal/interpreter/interfaces.ts index b4409ffa..a0691dc6 100644 --- a/src/internal/interpreter/interfaces.ts +++ b/src/internal/interpreter/interfaces.ts @@ -18,87 +18,42 @@ import { VariableExpressionDefinitionNode, } from '@superindustries/language'; -export interface MapParameters { - usecase?: string; - auth?: { - basic?: { - username: string; - password: string; - }; - bearer?: { - token: string; - }; - }; - baseUrl?: string; -} +export type Variables = { + [key: string]: string | Variables; +}; export interface MapVisitor { - visit( - node: MapASTNode, - parameters: MapParameters - ): Promise | unknown; - visitEvalDefinitionNode( - node: EvalDefinitionNode, - parameters: MapParameters - ): Promise | unknown; + visit(node: MapASTNode): Promise | unknown; + visitEvalDefinitionNode(node: EvalDefinitionNode): Promise | unknown; visitHTTPOperationDefinitionNode( - node: HTTPOperationDefinitionNode, - parameters: MapParameters + node: HTTPOperationDefinitionNode ): Promise | unknown; visitIterationDefinitionNode( - node: IterationDefinitionNode, - parameters: MapParameters - ): Promise | unknown; - visitJSExpressionNode( - node: JSExpressionNode, - parameters: MapParameters - ): Promise | unknown; - visitMapDefinitionNode( - node: MapDefinitionNode, - parameters: MapParameters - ): Promise | unknown; - visitMapDocumentNode( - node: MapDocumentNode, - parameters: MapParameters + node: IterationDefinitionNode ): Promise | unknown; + visitJSExpressionNode(node: JSExpressionNode): Promise | unknown; + visitMapDefinitionNode(node: MapDefinitionNode): Promise | unknown; + visitMapDocumentNode(node: MapDocumentNode): Promise | unknown; visitMapExpressionDefinitionNode( - node: MapExpressionDefinitionNode, - parameters: MapParameters - ): Promise | unknown; - visitMapNode( - node: MapNode, - parameters: MapParameters + node: MapExpressionDefinitionNode ): Promise | unknown; + visitMapNode(node: MapNode): Promise | unknown; visitNetworkOperationDefinitionNode( - node: NetworkOperationDefinitionNode, - parameters: MapParameters + node: NetworkOperationDefinitionNode ): Promise | unknown; visitOperationCallDefinitionNode( - node: OperationCallDefinitionNode, - parameters: MapParameters + node: OperationCallDefinitionNode ): Promise | unknown; visitOperationDefinitionNode( - node: OperationDefinitionNode, - parameters: MapParameters + node: OperationDefinitionNode ): Promise | unknown; visitOutcomeDefinitionNode( - node: OutcomeDefinitionNode, - parameters: MapParameters - ): Promise | unknown; - visitProfileIdNode( - node: MapProfileIdNode, - parameters: MapParameters - ): Promise | unknown; - visitProviderNode( - node: ProviderNode, - parameters: MapParameters - ): Promise | unknown; - visitStepDefinitionNode( - node: StepDefinitionNode, - parameters: MapParameters + node: OutcomeDefinitionNode ): Promise | unknown; + visitProfileIdNode(node: MapProfileIdNode): Promise | unknown; + visitProviderNode(node: ProviderNode): Promise | unknown; + visitStepDefinitionNode(node: StepDefinitionNode): Promise | unknown; visitVariableExpressionDefinitionNode( - node: VariableExpressionDefinitionNode, - parameters: MapParameters + node: VariableExpressionDefinitionNode ): Promise | unknown; } diff --git a/src/internal/interpreter/map-interpreter.test.ts b/src/internal/interpreter/map-interpreter.test.ts index 17b6ba4c..eb4024fe 100644 --- a/src/internal/interpreter/map-interpreter.test.ts +++ b/src/internal/interpreter/map-interpreter.test.ts @@ -6,10 +6,7 @@ import { MapInterpereter } from './map-interpreter'; const mockServer = getLocal(); describe('MapInterpreter', () => { - let interpreter: MapInterpereter; - beforeEach(async () => { - interpreter = new MapInterpereter(); await mockServer.start(); }); @@ -18,606 +15,588 @@ describe('MapInterpreter', () => { }); it('should fail with invalid AST', async () => { + const interpreter = new MapInterpereter({}); await expect( async () => - await interpreter.visit( - ({ kind: 'Invalid' } as unknown) as MapASTNode, - {} - ) + await interpreter.visit(({ kind: 'Invalid' } as unknown) as MapASTNode) ).rejects.toThrow(); }); it('should execute minimal Eval definition', async () => { - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - returnDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: '12', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: '12', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); it('should fail on undefined usecase', async () => { + const interpreter = new MapInterpereter({ usecase: 'nonexistent' }); await expect( async () => - await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - returnDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: '12', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: '12', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'nonexistent' } - ) + }, + ], + }, + ], + }) ).rejects.toThrow('Usecase not found.'); }); // This should not happen in practice, as the AST will be validated beforehand it('should fail when none of result/return/set are defined', async () => { + const interpreter = new MapInterpereter({ usecase: 'testCase' }); await expect( async () => - await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ) + }, + ], + }, + ], + }) ).rejects.toThrow('Something went very wrong, this should not happen!'); }); it('should execute Eval definition with variables', async () => { - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'x', - right: { - kind: 'JSExpression', - expression: '7', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'x', + right: { + kind: 'JSExpression', + expression: '7', }, - ], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - returnDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'x + 5', - }, + ], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'x + 5', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); it('should correctly resolve variable scope', async () => { - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'x', - right: { - kind: 'JSExpression', - expression: '8', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'x', + right: { + kind: 'JSExpression', + expression: '8', }, - ], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'x', - right: { - kind: 'JSExpression', - expression: '7', - }, + }, + ], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'x', + right: { + kind: 'JSExpression', + expression: '7', }, - ], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - returnDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'x + 5', - }, + ], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'x + 5', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); it('should run predefined operation', async () => { - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'OperationCallDefinition', - arguments: [], - operationName: 'my beloved operation', - successOutcomeDefinition: { - kind: 'OutcomeDefinition', - returnDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'variableFromOperation', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'OperationCallDefinition', + arguments: [], + operationName: 'my beloved operation', + successOutcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'variableFromOperation', }, - ], - }, + }, + ], }, }, - ], - }, - { - kind: 'OperationDefinition', - operationName: 'my beloved operation', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'step', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - setDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'variableFromOperation', - right: { - kind: 'JSExpression', - expression: '12', - }, + }, + ], + }, + { + kind: 'OperationDefinition', + operationName: 'my beloved operation', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'step', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'variableFromOperation', + right: { + kind: 'JSExpression', + expression: '12', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); it('should throw when trying to run undefined operation', async () => { + const interpreter = new MapInterpereter({ usecase: 'testCase' }); await expect( async () => - await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'OperationCallDefinition', - arguments: [], - operationName: 'my beloved operation', - successOutcomeDefinition: { - kind: 'OutcomeDefinition', - returnDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'variableFromOperation', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'OperationCallDefinition', + arguments: [], + operationName: 'my beloved operation', + successOutcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'variableFromOperation', }, - ], - }, + }, + ], }, }, - ], - }, - { - kind: 'OperationDefinition', - operationName: 'my not-so-beloved operation', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'step', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - setDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'variableFromOperation', - right: { - kind: 'JSExpression', - expression: '12', - }, + }, + ], + }, + { + kind: 'OperationDefinition', + operationName: 'my not-so-beloved operation', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'step', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'variableFromOperation', + right: { + kind: 'JSExpression', + expression: '12', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ) + }, + ], + }, + ], + }) ).rejects.toThrow('Operation my beloved operation not found'); }); it('should call an API', async () => { await mockServer.get('/twelve').thenJson(200, { data: 12 }); const url = mockServer.urlFor('/twelve'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'other', - queryParametersDefinition: [], + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); @@ -625,161 +604,364 @@ describe('MapInterpreter', () => { it('should call an API with relative URL', async () => { await mockServer.get('/twelve').thenJson(200, { data: 12 }); const baseUrl = mockServer.urlFor('/twelve').replace('/twelve', ''); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase', baseUrl }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url: '/twelve', - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/twelve', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'other', - queryParametersDefinition: [], + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase', baseUrl } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); it('should throw when calling an API with relative URL but not providing baseUrl', async () => { await mockServer.get('/twelve').thenJson(200, { data: 12 }); + const interpreter = new MapInterpereter({ usecase: 'testCase' }); await expect( async () => - await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/twelve', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], + }, + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }) + ).rejects.toThrow('Relative URL specified, but base URL not provided!'); + }); + + it('should call an API with path parameters', async () => { + await mockServer.get('/twelve/2').thenJson(200, { data: 144 }); + const url = mockServer.urlFor('/twelve'); + const interpreter = new MapInterpereter({ + usecase: 'testCase', + input: { page: '2' }, + }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'settingStep', + condition: { + kind: 'JSExpression', + expression: 'true', }, - provider: { - kind: 'Provider', - providerId: 'hi!', + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'page', + right: { + kind: 'JSExpression', + expression: 'input.page', + }, + }, + ], + }, }, }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'httpCallStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `${url}/{page}`, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url: '/twelve', - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, - }, - ], + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, + }, + }, + }, + ], + }, + ], + }); + + expect(result).toEqual({ result: 144 }); + }); + + it('should throw when calling an API with path parameters and some are missing', async () => { + const interpreter = new MapInterpereter({ + usecase: 'testCase', + }); + + await expect( + async () => + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'settingStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'page', + right: { + kind: 'JSExpression', + expression: '2', }, }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'other', - queryParametersDefinition: [], + ], + }, + }, + }, + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'httpCallStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: `//some.url/{missing}/{page}/{alsoMissing}`, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', + }, + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ) - ).rejects.toThrow('Relative URL specified, but base URL not provided!'); + }, + ], + }, + ], + }) + ).rejects.toThrow( + 'Values for URL replacement keys not found: missing, alsoMissing' + ); }); it('should call an API with parameters', async () => { @@ -788,97 +970,197 @@ describe('MapInterpreter', () => { .withQuery({ page: 2 }) .thenJson(200, { data: 144 }); const url = mockServer.urlFor('/twelve'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'pageNumber', - right: { - kind: 'JSExpression', - expression: '2', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'pageNumber', + right: { + kind: 'JSExpression', + expression: '2', }, - ], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + }, + ], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, + }, + ], }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'other', - queryParametersDefinition: [ + }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'page', + right: { + kind: 'JSExpression', + expression: 'pageNumber', + }, + }, + ], + }, + }, + }, + }, + ], + }, + ], + }); + + expect(result).toEqual({ result: 144 }); + }); + + it('should call an API with input', async () => { + await mockServer + .get('/twelve') + .withQuery({ page: 2 }) + .thenJson(200, { data: 144 }); + const url = mockServer.urlFor('/twelve'); + const interpreter = new MapInterpereter({ + usecase: 'testCase', + input: { page: '2' }, + }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'pageNumber', + right: { + kind: 'JSExpression', + expression: 'input.page', + }, + }, + ], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ { - kind: 'VariableExpressionsDefinition', - left: 'page', + kind: 'MapExpressionsDefinition', + left: 'result', right: { kind: 'JSExpression', - expression: 'pageNumber', + expression: 'body.data', }, }, ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'other', + queryParametersDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'page', + right: { + kind: 'JSExpression', + expression: 'pageNumber', + }, + }, + ], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 144 }); }); @@ -890,97 +1172,95 @@ describe('MapInterpreter', () => { .withHeaders({ someheader: 'hello' }) .thenJson(201, { bodyOk: true, headerOk: true }); const url = mockServer.urlFor('/checkBody'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'POST', - responseDefinition: { - statusCode: 201, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body', - }, - }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [ + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'POST', + responseDefinition: { + statusCode: 201, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ { kind: 'MapExpressionsDefinition', - left: 'anArray', - right: { - kind: 'JSExpression', - expression: '[1, 2, 3]', - }, - }, - ], - headers: [ - { - kind: 'VariableExpressionsDefinition', - left: 'SomeHeader', + left: 'result', right: { kind: 'JSExpression', - expression: '"hello"', + expression: 'body', }, }, ], - security: 'other', - queryParametersDefinition: [], }, }, + requestDefinition: { + contentType: 'application/json', + body: [ + { + kind: 'MapExpressionsDefinition', + left: 'anArray', + right: { + kind: 'JSExpression', + expression: '[1, 2, 3]', + }, + }, + ], + headers: [ + { + kind: 'VariableExpressionsDefinition', + left: 'SomeHeader', + right: { + kind: 'JSExpression', + expression: '"hello"', + }, + }, + ], + security: 'other', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: { @@ -997,144 +1277,142 @@ describe('MapInterpreter', () => { await mockServer.get('/second').thenJson(200, { secondStep: 5 }); const url1 = mockServer.urlFor('/first'); const url2 = mockServer.urlFor('/second'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'multistep' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'multistep', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'firstStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url: url1, - method: 'get', - requestDefinition: { - contentType: 'application/json', - queryParametersDefinition: [], - security: 'other', - headers: [], - body: [], - }, - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - setDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'someVariable', - right: { - kind: 'JSExpression', - expression: 'body.firstStep.someVar', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'multistep', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'firstStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: url1, + method: 'get', + requestDefinition: { + contentType: 'application/json', + queryParametersDefinition: [], + security: 'other', + headers: [], + body: [], + }, + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'someVariable', + right: { + kind: 'JSExpression', + expression: 'body.firstStep.someVar', }, - ], - }, + }, + ], }, }, }, }, - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'secondStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url: url2, - method: 'get', - requestDefinition: { - contentType: 'application/json', - queryParametersDefinition: [], - security: 'other', - headers: [], - body: [], - }, - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - setDefinition: [ - { - kind: 'VariableExpressionsDefinition', - left: 'someOtherVariable', - right: { - kind: 'JSExpression', - expression: 'body.secondStep', - }, + }, + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'secondStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: url2, + method: 'get', + requestDefinition: { + contentType: 'application/json', + queryParametersDefinition: [], + security: 'other', + headers: [], + body: [], + }, + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + setDefinition: [ + { + kind: 'VariableExpressionsDefinition', + left: 'someOtherVariable', + right: { + kind: 'JSExpression', + expression: 'body.secondStep', }, - ], - }, + }, + ], }, }, }, }, - { - kind: 'StepDefinition', - condition: { - kind: 'JSExpression', - expression: `typeof someOtherVariable !== 'undefined' && someOtherVariable && someOtherVariable < 10 && typeof someVariable !== undefined && someVariable && someVariable > 10`, - }, - variableExpressionsDefinition: [], - stepName: 'thirdStep', - run: { - kind: 'EvalDefinition', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'someVariable * someOtherVariable', - }, + }, + { + kind: 'StepDefinition', + condition: { + kind: 'JSExpression', + expression: `typeof someOtherVariable !== 'undefined' && someOtherVariable && someOtherVariable < 10 && typeof someVariable !== undefined && someVariable && someVariable > 10`, + }, + variableExpressionsDefinition: [], + stepName: 'thirdStep', + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'someVariable * someOtherVariable', }, - ], - }, + }, + ], }, }, - ], - }, - ], - }, - { usecase: 'multistep' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 * 5 }); }); @@ -1145,82 +1423,80 @@ describe('MapInterpreter', () => { .withHeaders({ Authorization: 'Basic bmFtZTpwYXNzd29yZA==' }) .thenJson(200, { data: 12 }); const url = mockServer.urlFor('/basic'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ + usecase: 'testCase', + auth: { basic: { username: 'name', password: 'password' } }, + }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'basic', - queryParametersDefinition: [], + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'basic', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { - usecase: 'testCase', - auth: { basic: { username: 'name', password: 'password' } }, - } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); @@ -1231,82 +1507,80 @@ describe('MapInterpreter', () => { .withHeaders({ Authorization: 'Bearer SuperSecret' }) .thenJson(200, { data: 12 }); const url = mockServer.urlFor('/bearer'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ + usecase: 'testCase', + auth: { bearer: { token: 'SuperSecret' } }, + }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'bearer', - queryParametersDefinition: [], + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'bearer', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { - usecase: 'testCase', - auth: { bearer: { token: 'SuperSecret' } }, - } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); @@ -1329,259 +1603,253 @@ describe('MapInterpreter', () => { return { json: { failed: true }, statusCode: 400 }; }); const url = mockServer.urlFor('/formdata'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'POST', - responseDefinition: { - statusCode: 201, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, - }, - ], - }, - }, - requestDefinition: { - contentType: 'multipart/form-data', - body: [ - { - kind: 'MapExpressionsDefinition', - left: 'formData', - right: { - kind: 'JSExpression', - expression: '"myFormData"', - }, - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'POST', + responseDefinition: { + statusCode: 201, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ { kind: 'MapExpressionsDefinition', - left: 'is', + left: 'result', right: { kind: 'JSExpression', - expression: '"present"', + expression: 'body.data', }, }, ], - headers: [], - security: 'other', - queryParametersDefinition: [], }, }, + requestDefinition: { + contentType: 'multipart/form-data', + body: [ + { + kind: 'MapExpressionsDefinition', + left: 'formData', + right: { + kind: 'JSExpression', + expression: '"myFormData"', + }, + }, + { + kind: 'MapExpressionsDefinition', + left: 'is', + right: { + kind: 'JSExpression', + expression: '"present"', + }, + }, + ], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12 }); }); it('should throw on an API with Basic auth, but without credentials', async () => { + const interpreter = new MapInterpereter({ + usecase: 'testCase', + }); await expect( async () => - await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url: '/unimportant', - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/unimportant', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'basic', - queryParametersDefinition: [], + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'basic', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { - usecase: 'testCase', - } - ) + }, + ], + }, + ], + }) ).rejects.toThrow(); }); it('should throw on an API with Bearer auth, but without credentials', async () => { + const interpreter = new MapInterpereter({ + usecase: 'testCase', + }); await expect( async () => - await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url: '/unimportant', - method: 'GET', - responseDefinition: { - statusCode: 200, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url: '/unimportant', + method: 'GET', + responseDefinition: { + statusCode: 200, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result', + right: { + kind: 'JSExpression', + expression: 'body.data', }, - ], - }, - }, - requestDefinition: { - contentType: 'application/json', - body: [], - headers: [], - security: 'bearer', - queryParametersDefinition: [], + }, + ], }, }, + requestDefinition: { + contentType: 'application/json', + body: [], + headers: [], + security: 'bearer', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { - usecase: 'testCase', - } - ) + }, + ], + }, + ], + }) ).rejects.toThrow(); }); @@ -1591,96 +1859,94 @@ describe('MapInterpreter', () => { .withForm({ form: 'is', o: 'k' }) .thenJson(201, { data: 12 }); const url = mockServer.urlFor('/urlencoded'); - const result = await interpreter.visit( - { - kind: 'MapDocument', - map: { - kind: 'Map', - profileId: { - kind: 'ProfileId', - profileId: 'hello!', - }, - provider: { - kind: 'Provider', - providerId: 'hi!', - }, + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', }, - definitions: [ - { - kind: 'MapDefinition', - mapName: 'testMap', - usecaseName: 'testCase', - variableExpressionsDefinition: [], - stepsDefinition: [ - { - kind: 'StepDefinition', - variableExpressionsDefinition: [], - stepName: 'oneAndOnlyStep', - condition: { - kind: 'JSExpression', - expression: 'true', - }, - iterationDefinition: { - kind: 'IterationDefinition', - }, - run: { - kind: 'NetworkOperationDefinition', - definition: { - kind: 'HTTPOperationDefinition', - variableExpressionsDefinition: [], - url, - method: 'POST', - responseDefinition: { - statusCode: 201, - contentType: 'application/json', - contentLanguage: 'en-US', - outcomeDefinition: { - kind: 'OutcomeDefinition', - resultDefinition: [ - { - kind: 'MapExpressionsDefinition', - left: 'result', - right: { - kind: 'JSExpression', - expression: 'body.data', - }, - }, - ], - }, - }, - requestDefinition: { - contentType: 'application/x-www-form-urlencoded', - body: [ - { - kind: 'MapExpressionsDefinition', - left: 'form', - right: { - kind: 'JSExpression', - expression: '"is"', - }, - }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'NetworkOperationDefinition', + definition: { + kind: 'HTTPOperationDefinition', + variableExpressionsDefinition: [], + url, + method: 'POST', + responseDefinition: { + statusCode: 201, + contentType: 'application/json', + contentLanguage: 'en-US', + outcomeDefinition: { + kind: 'OutcomeDefinition', + resultDefinition: [ { kind: 'MapExpressionsDefinition', - left: 'o', + left: 'result', right: { kind: 'JSExpression', - expression: '"k"', + expression: 'body.data', }, }, ], - headers: [], - security: 'other', - queryParametersDefinition: [], }, }, + requestDefinition: { + contentType: 'application/x-www-form-urlencoded', + body: [ + { + kind: 'MapExpressionsDefinition', + left: 'form', + right: { + kind: 'JSExpression', + expression: '"is"', + }, + }, + { + kind: 'MapExpressionsDefinition', + left: 'o', + right: { + kind: 'JSExpression', + expression: '"k"', + }, + }, + ], + headers: [], + security: 'other', + queryParametersDefinition: [], + }, }, }, - ], - }, - ], - }, - { usecase: 'testCase' } - ); + }, + ], + }, + ], + }); expect(result).toEqual({ result: 12, diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index c5b773f7..210107ef 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -22,16 +22,27 @@ import { import { evalScript } from '../../client/interpreter/Sandbox'; import { HttpClient } from '../http'; -import { MapParameters, MapVisitor } from './interfaces'; +import { MapVisitor, Variables } from './interfaces'; function assertUnreachable(node: never): never; function assertUnreachable(node: MapASTNode): never { throw new Error(`Invalid Node kind: ${node.kind}`); } -export type Variables = { - [key: string]: string | Variables; -}; +export interface MapParameters { + usecase?: string; + auth?: { + basic?: { + username: string; + password: string; + }; + bearer?: { + token: string; + }; + }; + baseUrl?: string; + input?: Variables; +} export class MapInterpereter implements MapVisitor { private variableStack: Variables[] = []; @@ -46,76 +57,68 @@ export class MapInterpereter implements MapVisitor { private mapScope: string | undefined; - async visit(node: MapASTNode, parameters: MapParameters): Promise { + constructor(private readonly parameters: MapParameters) {} + + async visit(node: MapASTNode): Promise { switch (node.kind) { case 'EvalDefinition': - return await this.visitEvalDefinitionNode(node, parameters); + return await this.visitEvalDefinitionNode(node); case 'HTTPOperationDefinition': - return this.visitHTTPOperationDefinitionNode(node, parameters); + return this.visitHTTPOperationDefinitionNode(node); case 'IterationDefinition': - return this.visitIterationDefinitionNode(node, parameters); + return this.visitIterationDefinitionNode(node); case 'JSExpression': - return this.visitJSExpressionNode(node, parameters); + return this.visitJSExpressionNode(node); case 'Map': - return this.visitMapNode(node, parameters); + return this.visitMapNode(node); case 'MapDefinition': - return this.visitMapDefinitionNode(node, parameters); + return this.visitMapDefinitionNode(node); case 'MapDocument': - return this.visitMapDocumentNode(node, parameters); + return this.visitMapDocumentNode(node); case 'MapExpressionsDefinition': - return this.visitMapExpressionDefinitionNode(node, parameters); + return this.visitMapExpressionDefinitionNode(node); case 'NetworkOperationDefinition': - return this.visitNetworkOperationDefinitionNode(node, parameters); + return this.visitNetworkOperationDefinitionNode(node); case 'OperationCallDefinition': - return this.visitOperationCallDefinitionNode(node, parameters); + return this.visitOperationCallDefinitionNode(node); case 'OperationDefinition': - return this.visitOperationDefinitionNode(node, parameters); + return this.visitOperationDefinitionNode(node); case 'OutcomeDefinition': - return this.visitOutcomeDefinitionNode(node, parameters); + return this.visitOutcomeDefinitionNode(node); case 'ProfileId': - return this.visitProfileIdNode(node, parameters); + return this.visitProfileIdNode(node); case 'Provider': - return this.visitProviderNode(node, parameters); + return this.visitProviderNode(node); case 'StepDefinition': - return this.visitStepDefinitionNode(node, parameters); + return this.visitStepDefinitionNode(node); case 'VariableExpressionsDefinition': - return this.visitVariableExpressionDefinitionNode(node, parameters); + return this.visitVariableExpressionDefinitionNode(node); default: assertUnreachable(node); } } - async visitEvalDefinitionNode( - node: EvalDefinitionNode, - parameters: MapParameters - ): Promise { - return this.visit(node.outcomeDefinition, parameters); + async visitEvalDefinitionNode(node: EvalDefinitionNode): Promise { + return this.visit(node.outcomeDefinition); } async visitHTTPOperationDefinitionNode( - node: HTTPOperationDefinitionNode, - parameters: MapParameters + node: HTTPOperationDefinitionNode ): Promise { const variables = await this.processVariableExpressions( - node.variableExpressionsDefinition, - parameters + node.variableExpressionsDefinition ); this.variableStack.push(variables); const queryParameters = await this.processVariableExpressions( - node.requestDefinition.queryParametersDefinition, - parameters + node.requestDefinition.queryParametersDefinition ); - const body = await this.processMapExpressions( - node.requestDefinition.body, - parameters - ); + const body = await this.processMapExpressions(node.requestDefinition.body); const headers = await this.processVariableExpressions( - node.requestDefinition.headers, - parameters + node.requestDefinition.headers ); const response = await HttpClient.request(node.url, { @@ -126,9 +129,12 @@ export class MapInterpereter implements MapVisitor { contentType: node.requestDefinition.contentType, accept: node.responseDefinition.contentType, security: node.requestDefinition.security, - basic: parameters.auth?.basic, - bearer: parameters.auth?.bearer, - baseUrl: parameters.baseUrl, + basic: this.parameters.auth?.basic, + bearer: this.parameters.auth?.bearer, + baseUrl: this.parameters.baseUrl, + pathParameters: this.mapScope + ? this.mapScopedVariables[this.mapScope] + : undefined, }); this.variableStack.push({ @@ -136,44 +142,33 @@ export class MapInterpereter implements MapVisitor { headers: response.headers, }); - return await this.visit( - node.responseDefinition.outcomeDefinition, - parameters - ); + return await this.visit(node.responseDefinition.outcomeDefinition); } visitIterationDefinitionNode( - _node: IterationDefinitionNode, - _parameters: MapParameters + _node: IterationDefinitionNode ): Promise | unknown { throw new Error('Method not implemented.'); } - async visitJSExpressionNode( - node: JSExpressionNode, - _parameters: MapParameters - ): Promise { + async visitJSExpressionNode(node: JSExpressionNode): Promise { return await evalScript(node.expression, this.variables); } - async visitMapDefinitionNode( - node: MapDefinitionNode, - parameters: MapParameters - ): Promise { + async visitMapDefinitionNode(node: MapDefinitionNode): Promise { this.mapScope = node.mapName; let result: unknown; for (const step of node.stepsDefinition) { - const condition = await this.visit(step.condition, parameters); + const condition = await this.visit(step.condition); if (condition) { const variables = await this.processVariableExpressions( - node.variableExpressionsDefinition, - parameters + node.variableExpressionsDefinition ); this.variableStack.push(variables); - const stepResult = await this.visit(step, parameters); + const stepResult = await this.visit(step); this.variableStack.pop(); if (stepResult) { @@ -187,49 +182,40 @@ export class MapInterpereter implements MapVisitor { return result; } - async visitMapDocumentNode( - node: MapDocumentNode, - parameters: MapParameters - ): Promise { + async visitMapDocumentNode(node: MapDocumentNode): Promise { this.operations = node.definitions.filter(isOperationDefinitionNode); const operation = node.definitions .filter(isMapDefinitionNode) - .find(definition => definition.usecaseName === parameters.usecase); + .find(definition => definition.usecaseName === this.parameters.usecase); if (!operation) { throw new Error('Usecase not found.'); } - return await this.visit(operation, parameters); + return await this.visit(operation); } async visitMapExpressionDefinitionNode( - node: MapExpressionDefinitionNode, - parameters: MapParameters + node: MapExpressionDefinitionNode ): Promise { return { - [node.left]: (await this.visit(node.right, parameters)) as string, + [node.left]: (await this.visit(node.right)) as string, }; } - visitMapNode( - _node: MapNode, - _parameters: MapParameters - ): Promise | unknown { + visitMapNode(_node: MapNode): Promise | unknown { throw new Error('Method not implemented.'); } visitNetworkOperationDefinitionNode( - node: NetworkOperationDefinitionNode, - parameters: MapParameters + node: NetworkOperationDefinitionNode ): Promise | unknown { - return this.visit(node.definition, parameters); + return this.visit(node.definition); } async visitOperationCallDefinitionNode( - node: OperationCallDefinitionNode, - parameters: MapParameters + node: OperationCallDefinitionNode ): Promise { const operation = this.operations.find( operation => operation.operationName === node.operationName @@ -239,12 +225,12 @@ export class MapInterpereter implements MapVisitor { throw new Error(`Operation ${node.operationName} not found!`); } - let result = await this.visit(operation, parameters); + let result = await this.visit(operation); this.operationScope = operation.operationName; if (!result) { - result = await this.visit(node.successOutcomeDefinition, parameters); + result = await this.visit(node.successOutcomeDefinition); } this.operationScope = undefined; @@ -253,23 +239,21 @@ export class MapInterpereter implements MapVisitor { } async visitOperationDefinitionNode( - node: OperationDefinitionNode, - parameters: MapParameters + node: OperationDefinitionNode ): Promise { this.operationScope = node.operationName; let result: unknown; for (const step of node.stepsDefinition) { - const condition = await this.visit(step.condition, parameters); + const condition = await this.visit(step.condition); if (condition) { const variables = await this.processVariableExpressions( - node.variableExpressionsDefinition, - parameters + node.variableExpressionsDefinition ); this.variableStack.push(variables); - const stepResult = await this.visit(step, parameters); + const stepResult = await this.visit(step); this.variableStack.pop(); if (stepResult) { @@ -284,81 +268,57 @@ export class MapInterpereter implements MapVisitor { } async visitOutcomeDefinitionNode( - node: OutcomeDefinitionNode, - parameters: MapParameters + node: OutcomeDefinitionNode ): Promise { if (node.returnDefinition) { - return await this.processMapExpressions( - node.returnDefinition, - parameters - ); + return await this.processMapExpressions(node.returnDefinition); } else if (node.setDefinition) { if (this.operationScope) { this.operationScopedVariables[this.operationScope] = { ...(this.operationScopedVariables[this.operationScope] || {}), - ...(await this.processVariableExpressions( - node.setDefinition, - parameters - )), + ...(await this.processVariableExpressions(node.setDefinition)), }; return undefined; } else if (this.mapScope) { this.mapScopedVariables[this.mapScope] = { ...(this.mapScopedVariables[this.mapScope] || {}), - ...(await this.processVariableExpressions( - node.setDefinition, - parameters - )), + ...(await this.processVariableExpressions(node.setDefinition)), }; return undefined; } } else if (node.resultDefinition) { - return await this.processMapExpressions( - node.resultDefinition, - parameters - ); + return await this.processMapExpressions(node.resultDefinition); } throw new Error('Something went very wrong, this should not happen!'); } - visitProfileIdNode( - _node: MapProfileIdNode, - _parameters: MapParameters - ): Promise | unknown { + visitProfileIdNode(_node: MapProfileIdNode): Promise | unknown { throw new Error('Method not implemented.'); } - visitProviderNode( - _node: ProviderNode, - _parameters: MapParameters - ): Promise | unknown { + visitProviderNode(_node: ProviderNode): Promise | unknown { throw new Error('Method not implemented.'); } - async visitStepDefinitionNode( - node: StepDefinitionNode, - parameters: MapParameters - ): Promise { + async visitStepDefinitionNode(node: StepDefinitionNode): Promise { const variables = await this.processVariableExpressions( - node.variableExpressionsDefinition, - parameters + node.variableExpressionsDefinition ); this.variableStack.push(variables); - const result = await this.visit(node.run, parameters); + const result = await this.visit(node.run); this.variableStack.pop(); return result; } async visitVariableExpressionDefinitionNode( - node: VariableExpressionDefinitionNode, - parameters: MapParameters + node: VariableExpressionDefinitionNode ): Promise { return { - [node.left]: (await this.visit(node.right, parameters)) as string, + [node.left]: (await this.visit(node.right)) as string, }; } @@ -388,19 +348,20 @@ export class MapInterpereter implements MapVisitor { }; } + variables = { + ...variables, + input: this.parameters.input ?? {}, + }; + return variables; } private async processVariableExpressions( - expressions: VariableExpressionDefinitionNode[], - parameters: MapParameters + expressions: VariableExpressionDefinitionNode[] ): Promise> { let variables: Record = {}; for (const expression of expressions) { - const result = (await this.visit(expression, parameters)) as Record< - string, - string - >; + const result = (await this.visit(expression)) as Record; variables = { ...variables, ...result }; } @@ -408,15 +369,11 @@ export class MapInterpereter implements MapVisitor { } private async processMapExpressions( - expressions: MapExpressionDefinitionNode[], - parameters: MapParameters + expressions: MapExpressionDefinitionNode[] ): Promise> { let variables: Record = {}; for (const expression of expressions) { - const result = (await this.visit(expression, parameters)) as Record< - string, - string - >; + const result = (await this.visit(expression)) as Record; variables = { ...variables, ...result }; } From 7ccef33a78ede897eeb471991ae5e2cf83af1df2 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Thu, 23 Jul 2020 14:26:08 +0200 Subject: [PATCH 07/15] feat(map-interpreter): Add basic LHS merging --- src/internal/http.ts | 18 +++++- .../interpreter/map-interpreter.test.ts | 58 +++++++++++++++++++ src/internal/interpreter/map-interpreter.ts | 51 +++++++++++++--- 3 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/internal/http.ts b/src/internal/http.ts index c1db38c5..266b1b55 100644 --- a/src/internal/http.ts +++ b/src/internal/http.ts @@ -16,6 +16,18 @@ const JSON_CONTENT = 'application/json'; const URLENCODED_CONTENT = 'application/x-www-form-urlencoded'; const FORMDATA_CONTENT = 'multipart/form-data'; +const variablesToStrings = (variables?: Variables): Record => { + const result: Record = {}; + + if (variables) { + for (const [key, value] of Object.entries(variables)) { + result[key] = typeof value === 'string' ? value : JSON.stringify(value); + } + } + + return result; +}; + const queryParameters = (parameters?: Record): string => { if (parameters && Object.keys(parameters).length) { return '?' + new URLSearchParams(parameters).toString(); @@ -108,7 +120,7 @@ export const HttpClient = { method: string; headers?: Record; queryParameters?: Record; - body?: Record; + body?: Variables; contentType?: string; accept?: string; security?: 'basic' | 'bearer' | 'other'; @@ -135,10 +147,10 @@ export const HttpClient = { params.body = JSON.stringify(parameters.body); } else if (parameters.contentType === URLENCODED_CONTENT) { headers.append('Content-Type', URLENCODED_CONTENT); - params.body = new URLSearchParams(parameters.body); + params.body = new URLSearchParams(variablesToStrings(parameters.body)); } else if (parameters.contentType === FORMDATA_CONTENT) { headers.append('Content-Type', FORMDATA_CONTENT); - params.body = formData(parameters.body); + params.body = formData(variablesToStrings(parameters.body)); } else { throw new Error(`Unknown content type: ${parameters.contentType}`); } diff --git a/src/internal/interpreter/map-interpreter.test.ts b/src/internal/interpreter/map-interpreter.test.ts index eb4024fe..6a304adf 100644 --- a/src/internal/interpreter/map-interpreter.test.ts +++ b/src/internal/interpreter/map-interpreter.test.ts @@ -1952,4 +1952,62 @@ describe('MapInterpreter', () => { result: 12, }); }); + + it('should execute Eval definition with nested result', async () => { + const interpreter = new MapInterpereter({ usecase: 'testCase' }); + const result = await interpreter.visit({ + kind: 'MapDocument', + map: { + kind: 'Map', + profileId: { + kind: 'ProfileId', + profileId: 'hello!', + }, + provider: { + kind: 'Provider', + providerId: 'hi!', + }, + }, + definitions: [ + { + kind: 'MapDefinition', + mapName: 'testMap', + usecaseName: 'testCase', + variableExpressionsDefinition: [], + stepsDefinition: [ + { + kind: 'StepDefinition', + variableExpressionsDefinition: [], + stepName: 'oneAndOnlyStep', + condition: { + kind: 'JSExpression', + expression: 'true', + }, + iterationDefinition: { + kind: 'IterationDefinition', + }, + run: { + kind: 'EvalDefinition', + outcomeDefinition: { + kind: 'OutcomeDefinition', + returnDefinition: [ + { + kind: 'MapExpressionsDefinition', + left: 'result.which.is.nested', + right: { + kind: 'JSExpression', + expression: '12', + }, + }, + ], + }, + }, + }, + ], + }, + ], + }); + + expect(result).toEqual({ result: { which: { is: { nested: 12 } } } }); + }); }); diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index 210107ef..a5a76a0b 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -44,6 +44,32 @@ export interface MapParameters { input?: Variables; } +export const mergeVariables = ( + left: Variables, + right: Variables +): Variables => { + const result: Variables = {}; + + if (typeof left === 'object') { + for (const key of Object.keys(left)) { + result[key] = left[key]; + } + for (const key of Object.keys(right)) { + const l = left[key]; + const r = right[key]; + if (typeof r !== 'string' && typeof l !== 'string') { + result[key] = mergeVariables(l, r); + } else { + result[key] = right[key]; + } + } + } else { + return right; + } + + return result; +}; + export class MapInterpereter implements MapVisitor { private variableStack: Variables[] = []; @@ -199,9 +225,20 @@ export class MapInterpereter implements MapVisitor { async visitMapExpressionDefinitionNode( node: MapExpressionDefinitionNode ): Promise { - return { - [node.left]: (await this.visit(node.right)) as string, - }; + const value = (await this.visit(node.right)) as string; + const path = node.left.split('.'); + const result: Variables = {}; + let current: Variables = result; + + for (let i = 0; i < path.length; ++i) { + if (i !== path.length - 1) { + current = current[path[i]] = {}; + } else { + current[path[i]] = value; + } + } + + return result; } visitMapNode(_node: MapNode): Promise | unknown { @@ -370,11 +407,11 @@ export class MapInterpereter implements MapVisitor { private async processMapExpressions( expressions: MapExpressionDefinitionNode[] - ): Promise> { - let variables: Record = {}; + ): Promise { + let variables: Variables = {}; for (const expression of expressions) { - const result = (await this.visit(expression)) as Record; - variables = { ...variables, ...result }; + const result = (await this.visit(expression)) as Variables; + variables = mergeVariables(variables, result); } return variables; From 932884c77324100d235865a6f1d878c95ad4cd94 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Thu, 23 Jul 2020 14:33:08 +0200 Subject: [PATCH 08/15] chore: Update language package version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1baaecc6..d2a95222 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "typescript": "^3.9.6" }, "dependencies": { - "@superindustries/language": "^0.0.7", + "@superindustries/language": "^0.0.10", "cross-fetch": "^3.0.5", "isomorphic-form-data": "^2.0.0", "vm2": "^3.9.2" diff --git a/yarn.lock b/yarn.lock index a492e9a3..4036f806 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,10 +1111,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@superindustries/language@^0.0.7": - version "0.0.7" - resolved "https://npm.pkg.github.com/download/@superindustries/language/0.0.7/bc74553cadd86b344d6cfbaa1e91e9f803a6695dfe4a96d3c62019c1975fe319#7c36984021ad4bfd93d0ad2dc315af583ccfc301" - integrity sha512-QpuyweFofet5TUGwPGLBLdYAeELxMpZ3Wa9xzQSjT2UC31RWVFankN5u8ng+WxmBT8NVl6VCAp2P3B6O+7QFNw== +"@superindustries/language@^0.0.10": + version "0.0.10" + resolved "https://npm.pkg.github.com/download/@superindustries/language/0.0.10/b6c603cb1837a40eb7bb969b0241842586b82005a276a619134ac696f20f8adc#e62fc060b790ca0252ce59f2b221cde4999f4771" + integrity sha512-5Tk4h8DVkqo7qHx3/JFzhml2lkft9dLgcxKa/Y3SjGOYypgNk1d8LJvLNAQrp+0dmWBQzJkb7u0ZgnA6k2Y0qQ== "@types/babel__core@^7.1.7": version "7.1.7" From 2cbbc87e92edde3cc6b8769777f3f9fbb141c261 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Thu, 23 Jul 2020 14:39:18 +0200 Subject: [PATCH 09/15] chore(ci-cd): Add Github bot token to actions --- .github/workflows/main.yml | 6 ++++++ .github/workflows/publish.yml | 2 ++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73eee9ea..1f140e6b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,8 @@ jobs: # Install and run tests - name: Install dependencies run: yarn install + env: + NODE_AUTH_TOKEN: ${{ secrets.SUPERFACE_BOT_PAT }} - name: Test run: yarn test @@ -54,6 +56,8 @@ jobs: # Install and run lint - name: Install dependencies run: yarn install + env: + NODE_AUTH_TOKEN: ${{ secrets.SUPERFACE_BOT_PAT }} - name: Lint run: yarn lint @@ -81,6 +85,8 @@ jobs: # Install and run license checker - name: Install dependencies run: yarn install + env: + NODE_AUTH_TOKEN: ${{ secrets.SUPERFACE_BOT_PAT }} - name: Install License checker run: | yarn global add license-checker diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2fef2854..b4549bf2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,6 +20,8 @@ jobs: # Install dependencies and run test - name: Install dependencies run: yarn install + env: + NODE_AUTH_TOKEN: ${{ secrets.SUPERFACE_BOT_PAT }} - name: Test run: yarn test From bb56d135bbbe957eb111759882adb98f8cf69193 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Thu, 23 Jul 2020 14:48:14 +0200 Subject: [PATCH 10/15] chore(ci-cd): Setup Github registry in actions --- .github/workflows/main.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1f140e6b..278938e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,6 +9,11 @@ jobs: # Setup environment and checkout the project master - name: Setup Node.js environment uses: actions/setup-node@v1.4.2 + with: + registry-url: "https://npm.pkg.github.com" + scope: "@superindustries" + always-auth: true + - name: Checkout uses: actions/checkout@v2 @@ -38,6 +43,11 @@ jobs: # Setup environment and checkout the project master - name: Setup Node.js environment uses: actions/setup-node@v1.4.2 + with: + registry-url: "https://npm.pkg.github.com" + scope: "@superindustries" + always-auth: true + - name: Checkout uses: actions/checkout@v2 @@ -67,6 +77,11 @@ jobs: # Setup environment and checkout the project master - name: Setup Node.js environment uses: actions/setup-node@v1.4.2 + with: + registry-url: "https://npm.pkg.github.com" + scope: "@superindustries" + always-auth: true + - name: Checkout uses: actions/checkout@v2 From b5c20e44e432f12a1c2bfe77bc2827fc0f2e1875 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Fri, 24 Jul 2020 09:26:17 +0200 Subject: [PATCH 11/15] chore(map-ast): remove unnecessary check in mergeVariables --- src/internal/interpreter/map-interpreter.ts | 24 +++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index a5a76a0b..741040c1 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -50,21 +50,17 @@ export const mergeVariables = ( ): Variables => { const result: Variables = {}; - if (typeof left === 'object') { - for (const key of Object.keys(left)) { - result[key] = left[key]; - } - for (const key of Object.keys(right)) { - const l = left[key]; - const r = right[key]; - if (typeof r !== 'string' && typeof l !== 'string') { - result[key] = mergeVariables(l, r); - } else { - result[key] = right[key]; - } + for (const key of Object.keys(left)) { + result[key] = left[key]; + } + for (const key of Object.keys(right)) { + const l = left[key]; + const r = right[key]; + if (typeof r !== 'string' && typeof l !== 'string') { + result[key] = mergeVariables(l, r); + } else { + result[key] = right[key]; } - } else { - return right; } return result; From 1f6dcf3edbecce4d4fe2612f361e535449495db1 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Fri, 24 Jul 2020 10:56:05 +0200 Subject: [PATCH 12/15] chore(map-ast): fix condition in mergeVariables --- src/internal/interpreter/map-interpreter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index 741040c1..f18620c3 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -56,7 +56,7 @@ export const mergeVariables = ( for (const key of Object.keys(right)) { const l = left[key]; const r = right[key]; - if (typeof r !== 'string' && typeof l !== 'string') { + if (typeof r !== 'string' && typeof l === 'object') { result[key] = mergeVariables(l, r); } else { result[key] = right[key]; From 39f1ae0ac6bb72f311bcbee2e2c66631c107afe2 Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Fri, 24 Jul 2020 14:25:26 +0200 Subject: [PATCH 13/15] chore(map-ast): fix some minor nits --- src/internal/interpreter/map-interpreter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index f18620c3..e5651284 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -84,7 +84,7 @@ export class MapInterpereter implements MapVisitor { async visit(node: MapASTNode): Promise { switch (node.kind) { case 'EvalDefinition': - return await this.visitEvalDefinitionNode(node); + return this.visitEvalDefinitionNode(node); case 'HTTPOperationDefinition': return this.visitHTTPOperationDefinitionNode(node); case 'IterationDefinition': @@ -308,14 +308,14 @@ export class MapInterpereter implements MapVisitor { } else if (node.setDefinition) { if (this.operationScope) { this.operationScopedVariables[this.operationScope] = { - ...(this.operationScopedVariables[this.operationScope] || {}), + ...(this.operationScopedVariables[this.operationScope] ?? {}), ...(await this.processVariableExpressions(node.setDefinition)), }; return undefined; } else if (this.mapScope) { this.mapScopedVariables[this.mapScope] = { - ...(this.mapScopedVariables[this.mapScope] || {}), + ...(this.mapScopedVariables[this.mapScope] ?? {}), ...(await this.processVariableExpressions(node.setDefinition)), }; From 110e175d693b204f9320e46e00f3c7226914020b Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Tue, 28 Jul 2020 12:21:30 +0200 Subject: [PATCH 14/15] refactor(map-interpreter): Make interpreter types stronger --- .eslintrc.js | 3 +- src/internal/http.ts | 16 ++--- src/internal/interpreter/interfaces.ts | 2 +- src/internal/interpreter/map-interpreter.ts | 78 ++++++++++++--------- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index de643e28..e0fc9fa0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,7 +29,8 @@ module.exports = { 'import/newline-after-import': 'error', 'import/no-duplicates': 'error', 'no-multiple-empty-lines': 'error', - 'lines-between-class-members': 'error', + 'lines-between-class-members': 'off', + '@typescript-eslint/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true, exceptAfterOverload: true }], }, settings: { 'import/parsers': { diff --git a/src/internal/http.ts b/src/internal/http.ts index 266b1b55..24364940 100644 --- a/src/internal/http.ts +++ b/src/internal/http.ts @@ -65,14 +65,14 @@ const formData = (data?: Record): FormData => { return formData; }; -const createUrl = async ( +const createUrl = ( inputUrl: string, parameters: { baseUrl?: string; pathParameters?: Variables; queryParameters?: Record; } -): Promise => { +): string => { const query = queryParameters(parameters.queryParameters); const isRelative = /^\/[^/]/.test(inputUrl); @@ -105,7 +105,7 @@ const createUrl = async ( for (const param of pathParameters) { url = url.replace( `{${param}}`, - (await evalScript(param, parameters.pathParameters)) as string + evalScript(param, parameters.pathParameters) ); } } @@ -118,8 +118,8 @@ export const HttpClient = { url: string, parameters: { method: string; - headers?: Record; - queryParameters?: Record; + headers?: Variables; + queryParameters?: Variables; body?: Variables; contentType?: string; accept?: string; @@ -130,7 +130,7 @@ export const HttpClient = { pathParameters?: Variables; } ): Promise => { - const headers = new Headers(parameters?.headers); + const headers = new Headers(variablesToStrings(parameters?.headers)); headers.append('Accept', parameters.accept ?? '*/*'); const params: RequestInit = { @@ -164,10 +164,10 @@ export const HttpClient = { const response = await fetch( encodeURI( - await createUrl(url, { + createUrl(url, { baseUrl: parameters.baseUrl, pathParameters: parameters.pathParameters, - queryParameters: parameters.queryParameters, + queryParameters: variablesToStrings(parameters.queryParameters), }) ), params diff --git a/src/internal/interpreter/interfaces.ts b/src/internal/interpreter/interfaces.ts index a0691dc6..45a22e9c 100644 --- a/src/internal/interpreter/interfaces.ts +++ b/src/internal/interpreter/interfaces.ts @@ -19,7 +19,7 @@ import { } from '@superindustries/language'; export type Variables = { - [key: string]: string | Variables; + [key: string]: string | Variables | undefined; }; export interface MapVisitor { diff --git a/src/internal/interpreter/map-interpreter.ts b/src/internal/interpreter/map-interpreter.ts index e5651284..32bd73ff 100644 --- a/src/internal/interpreter/map-interpreter.ts +++ b/src/internal/interpreter/map-interpreter.ts @@ -56,7 +56,7 @@ export const mergeVariables = ( for (const key of Object.keys(right)) { const l = left[key]; const r = right[key]; - if (typeof r !== 'string' && typeof l === 'object') { + if (r && typeof r !== 'string' && typeof l === 'object') { result[key] = mergeVariables(l, r); } else { result[key] = right[key]; @@ -68,20 +68,22 @@ export const mergeVariables = ( export class MapInterpereter implements MapVisitor { private variableStack: Variables[] = []; - private operations: OperationDefinitionNode[] = []; - private operationScopedVariables: Record = {}; - private operationScope: string | undefined; - private mapScopedVariables: Record = {}; - private mapScope: string | undefined; constructor(private readonly parameters: MapParameters) {} - async visit(node: MapASTNode): Promise { + async visit( + node: OutcomeDefinitionNode | HTTPOperationDefinitionNode + ): Promise; + async visit( + node: VariableExpressionDefinitionNode | MapExpressionDefinitionNode + ): Promise; + async visit(node: MapASTNode): Promise; + async visit(node: MapASTNode): Promise { switch (node.kind) { case 'EvalDefinition': return this.visitEvalDefinitionNode(node); @@ -121,13 +123,15 @@ export class MapInterpereter implements MapVisitor { } } - async visitEvalDefinitionNode(node: EvalDefinitionNode): Promise { - return this.visit(node.outcomeDefinition); + async visitEvalDefinitionNode( + node: EvalDefinitionNode + ): Promise { + return await this.visit(node.outcomeDefinition); } async visitHTTPOperationDefinitionNode( node: HTTPOperationDefinitionNode - ): Promise { + ): Promise { const variables = await this.processVariableExpressions( node.variableExpressionsDefinition ); @@ -167,20 +171,20 @@ export class MapInterpereter implements MapVisitor { return await this.visit(node.responseDefinition.outcomeDefinition); } - visitIterationDefinitionNode( - _node: IterationDefinitionNode - ): Promise | unknown { + visitIterationDefinitionNode(_node: IterationDefinitionNode): never { throw new Error('Method not implemented.'); } - async visitJSExpressionNode(node: JSExpressionNode): Promise { - return await evalScript(node.expression, this.variables); + visitJSExpressionNode(node: JSExpressionNode): string { + return evalScript(node.expression, this.variables); } - async visitMapDefinitionNode(node: MapDefinitionNode): Promise { + async visitMapDefinitionNode( + node: MapDefinitionNode + ): Promise { this.mapScope = node.mapName; - let result: unknown; + let result: string | Variables | undefined; for (const step of node.stepsDefinition) { const condition = await this.visit(step.condition); @@ -204,7 +208,9 @@ export class MapInterpereter implements MapVisitor { return result; } - async visitMapDocumentNode(node: MapDocumentNode): Promise { + async visitMapDocumentNode( + node: MapDocumentNode + ): Promise { this.operations = node.definitions.filter(isOperationDefinitionNode); const operation = node.definitions @@ -220,8 +226,8 @@ export class MapInterpereter implements MapVisitor { async visitMapExpressionDefinitionNode( node: MapExpressionDefinitionNode - ): Promise { - const value = (await this.visit(node.right)) as string; + ): Promise { + const value = await this.visit(node.right); const path = node.left.split('.'); const result: Variables = {}; let current: Variables = result; @@ -237,19 +243,19 @@ export class MapInterpereter implements MapVisitor { return result; } - visitMapNode(_node: MapNode): Promise | unknown { + visitMapNode(_node: MapNode): never { throw new Error('Method not implemented.'); } visitNetworkOperationDefinitionNode( node: NetworkOperationDefinitionNode - ): Promise | unknown { + ): Promise { return this.visit(node.definition); } async visitOperationCallDefinitionNode( node: OperationCallDefinitionNode - ): Promise { + ): Promise { const operation = this.operations.find( operation => operation.operationName === node.operationName ); @@ -273,10 +279,10 @@ export class MapInterpereter implements MapVisitor { async visitOperationDefinitionNode( node: OperationDefinitionNode - ): Promise { + ): Promise { this.operationScope = node.operationName; - let result: unknown; + let result: string | Variables | undefined; for (const step of node.stepsDefinition) { const condition = await this.visit(step.condition); @@ -302,7 +308,7 @@ export class MapInterpereter implements MapVisitor { async visitOutcomeDefinitionNode( node: OutcomeDefinitionNode - ): Promise { + ): Promise { if (node.returnDefinition) { return await this.processMapExpressions(node.returnDefinition); } else if (node.setDefinition) { @@ -327,15 +333,17 @@ export class MapInterpereter implements MapVisitor { throw new Error('Something went very wrong, this should not happen!'); } - visitProfileIdNode(_node: MapProfileIdNode): Promise | unknown { + visitProfileIdNode(_node: MapProfileIdNode): never { throw new Error('Method not implemented.'); } - visitProviderNode(_node: ProviderNode): Promise | unknown { + visitProviderNode(_node: ProviderNode): never { throw new Error('Method not implemented.'); } - async visitStepDefinitionNode(node: StepDefinitionNode): Promise { + async visitStepDefinitionNode( + node: StepDefinitionNode + ): Promise { const variables = await this.processVariableExpressions( node.variableExpressionsDefinition ); @@ -349,9 +357,9 @@ export class MapInterpereter implements MapVisitor { async visitVariableExpressionDefinitionNode( node: VariableExpressionDefinitionNode - ): Promise { + ): Promise { return { - [node.left]: (await this.visit(node.right)) as string, + [node.left]: await this.visit(node.right), }; } @@ -391,10 +399,10 @@ export class MapInterpereter implements MapVisitor { private async processVariableExpressions( expressions: VariableExpressionDefinitionNode[] - ): Promise> { - let variables: Record = {}; + ): Promise { + let variables: Variables = {}; for (const expression of expressions) { - const result = (await this.visit(expression)) as Record; + const result = await this.visit(expression); variables = { ...variables, ...result }; } @@ -406,7 +414,7 @@ export class MapInterpereter implements MapVisitor { ): Promise { let variables: Variables = {}; for (const expression of expressions) { - const result = (await this.visit(expression)) as Variables; + const result = await this.visit(expression); variables = mergeVariables(variables, result); } From e30a7f8fa20459b4bdf618ae6f2e7a12df92506f Mon Sep 17 00:00:00 2001 From: Lukas Valenta Date: Tue, 28 Jul 2020 12:24:38 +0200 Subject: [PATCH 15/15] chore(sandbox): Fix eval return value --- src/client/interpreter/Sandbox.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/Sandbox.ts b/src/client/interpreter/Sandbox.ts index d5cb843c..fb780daa 100644 --- a/src/client/interpreter/Sandbox.ts +++ b/src/client/interpreter/Sandbox.ts @@ -7,7 +7,7 @@ export const SCRIPT_TIMEOUT = 100; export function evalScript( js: string, variableDefinitions?: Variables -): unknown { +): string { const vm = new VM({ sandbox: { ...variableDefinitions, @@ -28,7 +28,7 @@ export function evalScript( delete global.require // Forbidden delete global.process // Forbidden delete global.console // Forbidden/useless - + delete global.setTimeout delete global.setInterval delete global.setImmediate