Skip to content

Commit

Permalink
Merge pull request #6 from superindustries/feature/map-ast-visitor
Browse files Browse the repository at this point in the history
Feature/map ast visitor
  • Loading branch information
TheEdward162 authored Aug 18, 2020
2 parents a2d147e + e30a7f8 commit c3935cc
Show file tree
Hide file tree
Showing 15 changed files with 3,663 additions and 29 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ module.exports = {
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
'no-multiple-empty-lines': 'error',
'lines-between-class-members': 'off',
'@typescript-eslint/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true, exceptAfterOverload: true }],
},
settings: {
'import/parsers': {
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ jobs:
# Setup environment and checkout the project master
- name: Setup Node.js environment
uses: actions/[email protected]
with:
registry-url: "https://npm.pkg.github.com"
scope: "@superindustries"
always-auth: true

- name: Checkout
uses: actions/checkout@v2

Expand All @@ -27,6 +32,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

Expand All @@ -36,6 +43,11 @@ jobs:
# Setup environment and checkout the project master
- name: Setup Node.js environment
uses: actions/[email protected]
with:
registry-url: "https://npm.pkg.github.com"
scope: "@superindustries"
always-auth: true

- name: Checkout
uses: actions/checkout@v2

Expand All @@ -54,6 +66,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

Expand All @@ -63,6 +77,11 @@ jobs:
# Setup environment and checkout the project master
- name: Setup Node.js environment
uses: actions/[email protected]
with:
registry-url: "https://npm.pkg.github.com"
scope: "@superindustries"
always-auth: true

- name: Checkout
uses: actions/checkout@v2

Expand All @@ -81,6 +100,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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@superindustries:registry=https://npm.pkg.github.com
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"types": "dist/index.d.ts",
"repository": "https://github.com/superindustries/superface.git",
"author": "Superface Team",
"license": "MIT",
"private": false,
"files": [
"dist/**/*"
Expand Down Expand Up @@ -40,12 +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": {
"@superindustries/language": "^0.0.10",
"cross-fetch": "^3.0.5",
"isomorphic-form-data": "^2.0.0",
"vm2": "^3.9.2"
}
}
15 changes: 11 additions & 4 deletions src/client/interpreter/Sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { VM } from 'vm2';

import { Variables } from '../../internal/interpreter/interfaces';

export const SCRIPT_TIMEOUT = 100;

export function evalScript(js: string): unknown {
export function evalScript(
js: string,
variableDefinitions?: Variables
): string {
const vm = new VM({
sandbox: {},
sandbox: {
...variableDefinitions,
},
compiler: 'javascript',
wasm: false,
eval: false,
Expand All @@ -21,7 +28,7 @@ export function evalScript(js: string): unknown {
delete global.require // Forbidden
delete global.process // Forbidden
delete global.console // Forbidden/useless
delete global.setTimeout
delete global.setInterval
delete global.setImmediate
Expand Down Expand Up @@ -50,5 +57,5 @@ export function evalScript(js: string): unknown {
`
);

return vm.run(`'use strict'; ${js}`);
return vm.run(`'use strict';${js}`);
}
5 changes: 0 additions & 5 deletions src/index.test.ts

This file was deleted.

39 changes: 39 additions & 0 deletions src/internal/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getLocal } from 'mockttp';

import { HttpClient } from './http';

const mockServer = getLocal();

describe('HttpClient', () => {
beforeEach(async () => {
await mockServer.start();
});

afterEach(async () => {
await mockServer.stop();
});

it('gets basic response', async () => {
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 () => {
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' } });
});
});
195 changes: 195 additions & 0 deletions src/internal/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
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;
headers: Record<string, string>;
}

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 variablesToStrings = (variables?: Variables): Record<string, string> => {
const result: Record<string, string> = {};

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, string>): 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<string, string>): FormData => {
const formData = new FormData();

if (data) {
Object.entries(data).forEach(([key, value]) => formData.append(key, value));
}

return formData;
};

const createUrl = (
inputUrl: string,
parameters: {
baseUrl?: string;
pathParameters?: Variables;
queryParameters?: Record<string, string>;
}
): string => {
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}}`,
evalScript(param, parameters.pathParameters)
);
}
}

return `${url}${query}`;
};

export const HttpClient = {
request: async (
url: string,
parameters: {
method: string;
headers?: Variables;
queryParameters?: Variables;
body?: Variables;
contentType?: string;
accept?: string;
security?: 'basic' | 'bearer' | 'other';
basic?: { username: string; password: string };
bearer?: { token: string };
baseUrl?: string;
pathParameters?: Variables;
}
): Promise<HttpResponse> => {
const headers = new Headers(variablesToStrings(parameters?.headers));
headers.append('Accept', parameters.accept ?? '*/*');

const params: RequestInit = {
headers,
method: parameters.method,
};

if (
parameters.body &&
['post', 'put', 'patch'].includes(parameters.method.toLowerCase())
) {
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(variablesToStrings(parameters.body));
} else if (parameters.contentType === FORMDATA_CONTENT) {
headers.append('Content-Type', FORMDATA_CONTENT);
params.body = formData(variablesToStrings(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 response = await fetch(
encodeURI(
createUrl(url, {
baseUrl: parameters.baseUrl,
pathParameters: parameters.pathParameters,
queryParameters: variablesToStrings(parameters.queryParameters),
})
),
params
);

let body: unknown;

if (parameters.accept === JSON_CONTENT) {
body = await response.json();
} else {
body = await response.text();
}

const responseHeaders: Record<string, string> = {};
response.headers.forEach((key, value) => {
responseHeaders[key] = value;
});

return {
statusCode: response.status,
body,
headers: responseHeaders,
};
},
};
Loading

0 comments on commit c3935cc

Please sign in to comment.