Skip to content

Commit

Permalink
Augment req/res objects (fix github.com/vercel/next.js/issues/21749)
Browse files Browse the repository at this point in the history
  • Loading branch information
jnbooth committed Dec 30, 2021
1 parent f6527c4 commit daf7cb2
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 123 deletions.
43 changes: 42 additions & 1 deletion src/handler.js
Expand Up @@ -53,6 +53,18 @@ const intParam = function intParam(key, orElse) {
});
};

/**
* Drop-in replacement for Next's `.json()` function.
*
* Fixes Next.js issue [#21749](https://github.com/vercel/next.js/issues/21749).
*
* @this {import('next').NextApiResponse} - API response object
*/
const betterSendJSON = function betterSendJSON(obj) {
this.setHeader('Content-Type', 'application/json; charset=utf-8');
this.send(JSON.stringify(obj));
};

// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
const runMiddleware = (req, res, fn) => new Promise((resolve, reject) => {
Expand All @@ -64,21 +76,26 @@ const corsWare = cors();

/* eslint no-await-in-loop: ["off"] */
const middleware = async function middleware(req, res) {
res.json = betterSendJSON;
await runMiddleware(req, res, corsWare);
await runMiddleware(req, res, compressionWare);
req.dateParam = dateParam;
req.intParam = intParam;
req.numParam = numParam;
};

/**
* @param {{[method: string]: BuskrHandler<any>}} routes
* @returns {import('next').NextApiHandler}
*/
export default function handler(routes) {
return async function handle(req, res) {
await middleware(req, res);
const route = routes[req.method];
if (route === undefined) {
res.status(404).send();
} else {
try {
await middleware(req, res);
await route(req, res);
} catch (error) {
const { message, status } = error;
Expand All @@ -91,3 +108,27 @@ export default function handler(routes) {
}
};
}

// Typedefs

/**
* @typedef {Object} BuskrQueryMixins
* @property {(key: string, orElse?: Date) => Date} dateParam
* @property {(key: string, orElse?: number) => number} numParam
* @property {(key: string, orElse?: number) => number} intParam
*/

/** @typedef {import('next').NextApiRequest & BuskrQueryMixins} BuskrRequest */

/**
* @template T
* @typedef {import('next').NextApiResponse<T>} BuskrResponse<T>
*/

/**
* @template T
* @callback BuskrHandler
* @param {BuskrRequest} req
* @param {BuskrResponse<T>} res
* @returns {Promise<void>}
*/
27 changes: 14 additions & 13 deletions src/pages/api/calendar.js
Expand Up @@ -3,18 +3,19 @@ import EventController from '../../db/event';

const defaultDist = Number(process.env.NEXT_PUBLIC_VISIBLE_METERS);

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
const GET = async function GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const offset = req.numParam('offset', 0);
const dist = req.intParam('dist', defaultDist);
export default handler({
async GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const offset = req.numParam('offset', 0);
const dist = req.intParam('dist', defaultDist);

const dates = await EventController.findDates({ center, dist, offset, search: req.query.search });
const dates = await EventController.findDates({
center,
dist,
offset,
search: req.query.search,
});

res.status(200).json(dates);
};

export default handler({ GET });
res.status(200).json(dates);
},
});
24 changes: 10 additions & 14 deletions src/pages/api/conflicts.js
Expand Up @@ -3,19 +3,15 @@ import EventController from '../../db/event';

const defaultDist = Number(process.env.NEXT_PUBLIC_CONFLICT_METERS);

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
const GET = async function GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const from = req.dateParam('from');
const to = req.dateParam('to');
const dist = req.intParam('dist', defaultDist);
export default handler({
async GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const from = req.dateParam('from');
const to = req.dateParam('to');
const dist = req.intParam('dist', defaultDist);

const conflicts = await EventController.findConflicts({ center, from, to, dist });
const conflicts = await EventController.findConflicts({ center, from, to, dist });

res.status(200).json(conflicts.map(({ event }) => event));
};

export default handler({ GET });
res.status(200).json(conflicts.map(({ event }) => event));
},
});
112 changes: 52 additions & 60 deletions src/pages/api/events.js
Expand Up @@ -4,63 +4,55 @@ import { getUser } from '../../auth';

const defaultDist = Number(process.env.NEXT_PUBLIC_VISIBLE_METERS);

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
const GET = async function GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const dist = req.intParam('dist', defaultDist);
const from = req.dateParam('from', null);
const to = req.dateParam('to', null);
const limit = req.intParam('limit', null);
const offset = req.intParam('offset', null);

const { search, sort } = req.query;

let orderBy;
if (sort === undefined || sort === 'distance') {
orderBy = 'distance';
} else if (sort === 'time') {
orderBy = 'starts';
} else {
throw new HttpException(400, `unrecognized sort option ${sort}`, sort);
}

const events = await EventController.getMany({
center,
dist,
from,
to,
limit,
offset,
search,
orderBy,
});

res.status(200).json(events);
};

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
const POST = async function POST(req, res) {
const { body } = req;
if (!body.name) {
throw new HttpException(400, 'missing name');
}
if (!body.description) {
throw new HttpException(400, 'missing name');
}
const starts = new Date(req.body.starts);
const ends = new Date(req.body.ends);
if (ends < starts || starts < new Date()) {
throw new HttpException(422, 'out-of-bounds date');
}
const user = await getUser({ req });
const eventId = await EventController.create(user, body);
res.status(201).json({ id: eventId });
};

export default handler({ GET, POST });
export default handler({
async GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const dist = req.intParam('dist', defaultDist);
const from = req.dateParam('from', null);
const to = req.dateParam('to', null);
const limit = req.intParam('limit', null);
const offset = req.intParam('offset', null);

const { search, sort } = req.query;

let orderBy;
if (sort === undefined || sort === 'distance') {
orderBy = 'distance';
} else if (sort === 'time') {
orderBy = 'starts';
} else {
throw new HttpException(400, `unrecognized sort option ${sort}`, sort);
}

const events = await EventController.getMany({
center,
dist,
from,
to,
limit,
offset,
search,
orderBy,
});

res.status(200).json(events);
},

async POST(req, res) {
const { body } = req;
if (!body.name) {
throw new HttpException(400, 'missing name');
}
if (!body.description) {
throw new HttpException(400, 'missing name');
}
const starts = new Date(req.body.starts);
const ends = new Date(req.body.ends);
if (ends < starts || starts < new Date()) {
throw new HttpException(422, 'out-of-bounds date');
}
const user = await getUser({ req });
const eventId = await EventController.create(user, body);
res.status(201).json({ id: eventId });
},
});
27 changes: 11 additions & 16 deletions src/pages/api/locate.js
@@ -1,19 +1,14 @@
import handler from '../../handler';
import { locateIP } from '../../db/location';

/**
* @param {import('next').NextApiRequest} req
* @param {import('next').NextApiResponse} res
*/
const GET = async function GET(req, res) {
/** @type {string} */
let ip = req.socket.remoteAddress;
const lastColon = ip.lastIndexOf(':');
if (lastColon !== -1 && ip.indexOf('.') !== -1) { // mixed IPv4 and IPv6
ip = ip.substring(0, lastColon);
}
const location = await locateIP(ip);
res.status(200).json(location);
};

export default handler({ GET });
export default handler({
async GET(req, res) {
let ip = req.socket.remoteAddress;
const lastColon = ip.lastIndexOf(':');
if (lastColon !== -1 && ip.indexOf('.') !== -1) { // mixed IPv4 and IPv6
ip = ip.substring(0, lastColon);
}
const location = await locateIP(ip);
res.status(200).json(location);
},
});
34 changes: 15 additions & 19 deletions src/pages/api/suggestions.js
Expand Up @@ -3,24 +3,20 @@ import EventController from '../../db/event';

const defaultDist = Number(process.env.NEXT_PUBLIC_VISIBLE_METERS);

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
const GET = async function GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const dist = req.intParam('dist', defaultDist);
const from = req.dateParam('from');
const to = req.dateParam('to');
export default handler({
async GET(req, res) {
const center = { lng: req.numParam('lng'), lat: req.numParam('lat') };
const dist = req.intParam('dist', defaultDist);
const from = req.dateParam('from');
const to = req.dateParam('to');

const suggestions = await EventController.getSuggestions({
center,
dist,
from,
to,
});
const suggestions = await EventController.getSuggestions({
center,
dist,
from,
to,
});

res.status(200).json(suggestions);
};

export default handler({ GET });
res.status(200).json(suggestions);
},
});

0 comments on commit daf7cb2

Please sign in to comment.