Skip to content

Commit

Permalink
add support for request.formData
Browse files Browse the repository at this point in the history
  • Loading branch information
thescientist13 committed Aug 26, 2023
1 parent a4a22c6 commit c580243
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 56 deletions.
18 changes: 13 additions & 5 deletions packages/cli/src/lib/api-route-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@ async function responseAsObject (response) {
}

async function executeRouteModule({ href, request }) {
const { body, headers, method, url } = request;
const { body, headers = {}, method, url } = request;
const contentType = headers['content-type'] || '';
const { handler } = await import(new URL(href));
const format = contentType.startsWith('application/json')
? JSON.parse(body)
: body;

// handling of serialized FormData across Worker threads
if (contentType.startsWith('x-greenwood/www-form-urlencoded')) {
headers['content-type'] = 'application/x-www-form-urlencoded';
}

const response = await handler(transformKoaRequestIntoStandardRequest(new URL(url), {
method,
headers,
body: ['GET', 'HEAD'].includes(method)
? body
: JSON.parse(body)
header: headers,
body: format
}));

parentPort.postMessage(await responseAsObject(response));
Expand Down
28 changes: 26 additions & 2 deletions packages/cli/src/lib/resource-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,40 @@ function isLocalLink(url = '') {
return url !== '' && (url.indexOf('http') !== 0 && url.indexOf('//') !== 0);
}

// TODO handle full request
// https://github.com/ProjectEvergreen/greenwood/discussions/1146
function transformKoaRequestIntoStandardRequest(url, request) {
const { body, method, header } = request;
const headers = new Headers(header);
const contentType = headers.get('content-type') || '';
let format;

if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = new FormData();

for (const key of Object.keys(body)) {
formData.append(key, body[key]);
}

// when using FormData, let Request set the correct headers
// or else it will come out as multipart/form-data
// https://stackoverflow.com/a/43521052/417806
headers.delete('content-type');

format = formData;
} else if (contentType.includes('application/json')) {
format = JSON.stringify(body);
} else {
format = body;
}

// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters
return new Request(url, {
body: ['GET', 'HEAD'].includes(method.toUpperCase())
? null
: JSON.stringify(body),
: format,
method,
headers: new Headers(header)
headers
});
}

Expand Down
43 changes: 29 additions & 14 deletions packages/cli/src/plugins/resource/plugin-api-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ async function requestAsObject (_request) {
{ name: 'TypeError', message: 'Argument must be a Request object' }
);
}

const request = _request.clone();
const contentType = request.headers.get('content-type') || '';
let headers = Object.fromEntries(request.headers);
let format;

function stringifiableObject (obj) {
const filtered = {};
Expand All @@ -26,11 +30,30 @@ async function requestAsObject (_request) {
return filtered;
}

// TODO handle full request
if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = await request.formData();
const params = {};

for (const entry of formData.entries()) {
params[entry[0]] = entry[1];
}

// when using FormData, let Request set the correct headers
// or else it will come out as multipart/form-data
// for serialization between route workers, leave a special marker for Greenwood
// https://stackoverflow.com/a/43521052/417806
headers['content-type'] = 'x-greenwood/www-form-urlencoded';
format = JSON.stringify(params);
} else if (contentType.includes('application/json')) {
format = JSON.stringify(await request.json());
} else {
format = await request.text();
}

return {
...stringifiableObject(request),
body: await request.text(),
headers: Object.fromEntries(request.headers)
body: format,
headers
};
}

Expand All @@ -50,17 +73,9 @@ class ApiRoutesResource extends ResourceInterface {
const apiUrl = new URL(`.${api.path}`, this.compilation.context.userWorkspace);
const href = apiUrl.href;

// TODO does this ever run in anything but development mode?
if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle
const workerUrl = new URL('../../lib/api-route-worker.js', import.meta.url);
const { headers, method } = request;
const req = await requestAsObject(new Request(url, {
body: ['GET', 'HEAD'].includes(method.toUpperCase())
? null
: await request.text(),
method,
headers
}));
const req = await requestAsObject(request);

const response = await new Promise(async (resolve, reject) => {
const worker = new Worker(workerUrl);
Expand All @@ -77,10 +92,10 @@ class ApiRoutesResource extends ResourceInterface {

worker.postMessage({ href, request: req });
});
const { body, status, statusText } = response;
const { headers, body, status, statusText } = response;

return new Response(status === 204 ? null : body, {
headers: response.headers,
headers: new Headers(headers),
status,
statusText
});
Expand Down
57 changes: 50 additions & 7 deletions packages/cli/test/cases/develop.default/develop.default.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
* greeting.js
* missing.js
* nothing.js
* submit.js
* submit-form-data.js
* submit-json.js
* assets/
* data.json
* favicon.ico
Expand Down Expand Up @@ -1449,16 +1450,16 @@ describe('Develop Greenwood With: ', function() {
});
});

describe('Develop command with POST API specific behaviors', function() {
const name = 'Greenwood';
describe('Develop command with POST API specific behaviors for JSON', function() {
const param = 'Greenwood';
let response = {};

before(async function() {
return new Promise((resolve, reject) => {
request.post({
url: `${hostname}:${port}/api/submit`,
url: `${hostname}:${port}/api/submit-json`,
json: true,
body: { name }
body: { name: param }
}, (err, res, body) => {
if (err) {
reject();
Expand All @@ -1478,12 +1479,14 @@ describe('Develop Greenwood With: ', function() {
});

it('should return the expected response message', function(done) {
expect(response.body).to.equal(`Thank you ${name} for your submission!`);
const { message } = response.body;

expect(message).to.equal(`Thank you ${param} for your submission!`);
done();
});

it('should return the expected content type header', function(done) {
expect(response.headers['content-type']).to.equal('text/html');
expect(response.headers['content-type']).to.equal('application/json');
done();
});

Expand All @@ -1492,6 +1495,46 @@ describe('Develop Greenwood With: ', function() {
done();
});
});

describe('Develop command with POST API specific behaviors for FormData', function() {
const param = 'Greenwood';
let response = {};

before(async function() {
return new Promise((resolve, reject) => {
request.post({
url: `${hostname}:${port}/api/submit-form-data`,
form: {
name: param
}
}, (err, res, body) => {
if (err) {
reject();
}

response = res;
response.body = body;

resolve(response);
});
});
});

it('should return a 200 status', function(done) {
expect(response.statusCode).to.equal(200);
done();
});

it('should return the expected response message', function(done) {
expect(response.body).to.equal(`Thank you ${param} for your submission!`);
done();
});

it('should return the expected content type header', function(done) {
expect(response.headers['content-type']).to.equal('text/html');
done();
});
});
});

after(function() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export async function handler(request) {
const formData = await request.formData();
const name = formData.get('name');
const body = `Thank you ${name} for your submission!`;

return new Response(body, {
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
export async function handler(request) {
// TODO const formData = await request.formData()
const formData = await request.json();
const { name } = formData;
const body = `Thank you ${name} for your submission!`;
const body = { message: `Thank you ${name} for your submission!` };

return new Response(body, {
return new Response(JSON.stringify(body), {
headers: new Headers({
'Content-Type': 'text/html',
'Content-Type': 'application/json',
'x-secret': 1234
})
});
Expand Down
14 changes: 0 additions & 14 deletions packages/cli/test/cases/develop.default/src/api/submit.js

This file was deleted.

Loading

0 comments on commit c580243

Please sign in to comment.