Skip to content

Commit

Permalink
Do not log initialization info in prod (#99)
Browse files Browse the repository at this point in the history
* Do not log initialization info in prod

This change prevents loggin the initialization info
in prod environment, as the logging state is handled
in memory and serverless environments will log it on
every invocation of the docs route handlers.

This also refactors the project structure so that app
router and pages router specific code is split into
separate folders.

* Improve test coverage
  • Loading branch information
blomqma authored Nov 6, 2023
1 parent ff9a0af commit d0ce834
Show file tree
Hide file tree
Showing 17 changed files with 659 additions and 527 deletions.
66 changes: 66 additions & 0 deletions packages/next-rest-framework/src/app-router/docs-route-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type NextRequest, NextResponse } from 'next/server';
import { DEFAULT_ERRORS, NEXT_REST_FRAMEWORK_USER_AGENT } from '../constants';
import { type BaseQuery, type NextRestFrameworkConfig } from '../types';
import {
generatePathsFromDev,
getConfig,
syncOpenApiSpec,
logInitInfo,
logNextRestFrameworkError,
getHtmlForDocs
} from '../utils';

export const docsRouteHandler = (_config?: NextRestFrameworkConfig) => {
const config = getConfig(_config);

const handler = async (req: NextRequest, _context: { params: BaseQuery }) => {
try {
const { headers, nextUrl } = req;
const host = headers.get('host') ?? '';

if (process.env.NODE_ENV !== 'production') {
const proto = headers.get('x-forwarded-proto') ?? 'http';
const baseUrl = `${proto}://${host}`;
const url = baseUrl + nextUrl.pathname;

// Return 403 if called internally by the framework.
if (headers.get('user-agent') === NEXT_REST_FRAMEWORK_USER_AGENT) {
return NextResponse.json(
{
message: `${NEXT_REST_FRAMEWORK_USER_AGENT} user agent is not allowed.`
},
{ status: 403 }
);
}

if (!config.suppressInfo) {
logInitInfo({ config, baseUrl, url });
}

if (config.autoGenerateOpenApiSpec) {
const paths = await generatePathsFromDev({ config, baseUrl, url });
await syncOpenApiSpec({ config, paths });
}
}

const html = getHtmlForDocs({ config, host });

return new NextResponse(html, {
headers: {
'Content-Type': 'text/html'
},
status: 200
});
} catch (error) {
logNextRestFrameworkError(error);

return NextResponse.json(
{ message: DEFAULT_ERRORS.unexpectedError },
{ status: 500 }
);
}
};

handler.nextRestFrameworkConfig = config;
return handler;
};
3 changes: 3 additions & 0 deletions packages/next-rest-framework/src/app-router/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './docs-route-handler';
export * from './route-handler';
export * from './route-operation';
155 changes: 155 additions & 0 deletions packages/next-rest-framework/src/app-router/route-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server';
import { DEFAULT_ERRORS, NEXT_REST_FRAMEWORK_USER_AGENT } from '../constants';
import { type RouteParams, type BaseQuery } from '../types';
import {
getPathsFromMethodHandlers,
isValidMethod,
validateSchema,
logNextRestFrameworkError
} from '../utils';

export const routeHandler = (methodHandlers: RouteParams) => {
const handler = async (req: NextRequest, context: { params: BaseQuery }) => {
try {
const { method, headers, nextUrl } = req;
const { pathname } = nextUrl;

const handleMethodNotAllowed = () => {
return NextResponse.json(
{ message: DEFAULT_ERRORS.methodNotAllowed },
{
status: 405,
headers: {
Allow: Object.keys(methodHandlers).join(', ')
}
}
);
};

if (!isValidMethod(method)) {
return handleMethodNotAllowed();
}

if (
process.env.NODE_ENV !== 'production' &&
headers.get('user-agent') === NEXT_REST_FRAMEWORK_USER_AGENT
) {
const route = decodeURIComponent(pathname ?? '');

try {
const nextRestFrameworkPaths = getPathsFromMethodHandlers({
methodHandlers,
route
});

return NextResponse.json({ nextRestFrameworkPaths }, { status: 200 });
} catch (error) {
throw Error(`OpenAPI spec generation failed for route: ${route}
${error}`);
}
}

const methodHandler = methodHandlers[method];

if (!methodHandler) {
return handleMethodNotAllowed();
}

const { input, handler, middleware } = methodHandler._config;

if (middleware) {
const res = await middleware(new NextRequest(req.clone()), context);

if (res) {
return res;
}
}

if (input) {
const { body: bodySchema, query: querySchema, contentType } = input;

if (
contentType &&
headers.get('content-type')?.split(';')[0] !== contentType
) {
return NextResponse.json(
{ message: DEFAULT_ERRORS.invalidMediaType },
{ status: 415 }
);
}

if (bodySchema) {
try {
const reqClone = req.clone();
const body = await reqClone.json();

const { valid, errors } = await validateSchema({
schema: bodySchema,
obj: body
});

if (!valid) {
return NextResponse.json(
{
message: 'Invalid request body.',
errors
},
{
status: 400
}
);
}
} catch (error) {
return NextResponse.json(
{
message: 'Missing request body.'
},
{
status: 400
}
);
}
}
if (querySchema) {
const { valid, errors } = await validateSchema({
schema: querySchema,
obj: Object.fromEntries(new URLSearchParams(req.nextUrl.search))
});

if (!valid) {
return NextResponse.json(
{
message: 'Invalid query parameters.',
errors
},
{
status: 400
}
);
}
}
}

if (!handler) {
throw Error('Handler not found.');
}

const res = await handler(req, context);
return res;
} catch (error) {
logNextRestFrameworkError(error);
return NextResponse.json(
{ message: DEFAULT_ERRORS.unexpectedError },
{ status: 500 }
);
}
};

handler.getPaths = (route: string) =>
getPathsFromMethodHandlers({
methodHandlers,
route
});

return handler;
};
55 changes: 55 additions & 0 deletions packages/next-rest-framework/src/app-router/route-operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
type RouteOperation,
type RouteOperationDefinition,
type InputObject,
type OutputObject,
type NextRouteHandler
} from '../types';

export const routeOperation: RouteOperation = (openApiOperation) => {
const createConfig = <Middleware, Handler>(
input: InputObject | undefined,
output: readonly OutputObject[] | undefined,
middleware: Middleware | undefined,
handler: Handler | undefined
): RouteOperationDefinition => ({
_config: {
openApiOperation,
input,
output,
middleware: middleware as NextRouteHandler,
handler: handler as NextRouteHandler
}
});

return {
input: (input) => ({
output: (output) => ({
middleware: (middleware) => ({
handler: (handler) => createConfig(input, output, middleware, handler)
}),
handler: (handler) => createConfig(input, output, undefined, handler)
}),
middleware: (middleware) => ({
output: (output) => ({
handler: (handler) => createConfig(input, output, middleware, handler)
}),
handler: (handler) =>
createConfig(input, undefined, middleware, handler)
}),
handler: (handler) => createConfig(input, undefined, undefined, handler)
}),
output: (output) => ({
middleware: (middleware) => ({
handler: (handler) =>
createConfig(undefined, output, middleware, handler)
}),
handler: (handler) => createConfig(undefined, output, undefined, handler)
}),
middleware: (middleware) => ({
handler: (handler) =>
createConfig(undefined, undefined, middleware, handler)
}),
handler: (handler) => createConfig(undefined, undefined, undefined, handler)
};
};
Loading

2 comments on commit d0ce834

@vercel
Copy link

@vercel vercel bot commented on d0ce834 Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-rest-framework-demo – ./apps/example

next-rest-framework-demo.vercel.app
next-rest-framework-demo-blomqma.vercel.app
next-rest-framework-demo-git-main-blomqma.vercel.app

@vercel
Copy link

@vercel vercel bot commented on d0ce834 Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-rest-framework – ./docs

next-rest-framework.vercel.app
next-rest-framework-git-main-blomqma.vercel.app
next-rest-framework-blomqma.vercel.app

Please sign in to comment.