Skip to content

Commit

Permalink
feat(app-builder): add support for raw uuids in decision routes (#362)
Browse files Browse the repository at this point in the history
* feat(app-builder): add http status codes

* fix(i18n): add missing translations to fix hydration issue

* feat(app-builder): add support for raw uuids in decision routes

* refactor(http): define http responses
  • Loading branch information
balzdur authored Feb 9, 2024
1 parent bd5afe9 commit 6a24940
Show file tree
Hide file tree
Showing 7 changed files with 482 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import { filtersI18n } from '../Filters/filters-i18n';
export const decisionsI18n = [
'decisions',
'common',
'cases',
...filtersI18n,
] satisfies Namespace;
14 changes: 10 additions & 4 deletions packages/app-builder/src/models/http-errors.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import {
BAD_REQUEST,
CONFLICT,
FORBIDDEN,
NOT_FOUND,
} from '@app-builder/utils/http/http-status-codes';
import { type HttpError } from 'oazapfts';

export function isHttpError(error: unknown): error is HttpError {
return error instanceof Error && 'status' in error;
}

export function isStatusConflictHttpError(error: unknown): error is HttpError {
return isHttpError(error) && error.status === 409;
return isHttpError(error) && error.status === CONFLICT;
}

export function isStatusBadRequestHttpError(
error: unknown,
): error is HttpError {
return isHttpError(error) && error.status === 400;
return isHttpError(error) && error.status === BAD_REQUEST;
}

export function isNotFoundHttpError(error: unknown): error is HttpError {
return isHttpError(error) && error.status === 404;
return isHttpError(error) && error.status === NOT_FOUND;
}

export function isForbiddenHttpError(error: unknown): error is HttpError {
return isHttpError(error) && error.status === 403;
return isHttpError(error) && error.status === FORBIDDEN;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import { ScorePanel } from '@app-builder/components/Decisions/Score';
import { TriggerObjectDetail } from '@app-builder/components/Decisions/TriggerObjectDetail';
import { isNotFoundHttpError } from '@app-builder/models';
import { serverServices } from '@app-builder/services/init.server';
import { handleParseParamError } from '@app-builder/utils/http/handle-errors';
import { notFound } from '@app-builder/utils/http/http-responses';
import { parseParamsSafe } from '@app-builder/utils/input-validation';
import { getRoute } from '@app-builder/utils/routes';
import { fromParams } from '@app-builder/utils/short-uuid';
import { shortUUIDSchema } from '@app-builder/utils/schema/shortUUIDSchema';
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import {
isRouteErrorResponse,
Expand All @@ -28,6 +31,7 @@ import { type Namespace } from 'i18next';
import { useTranslation } from 'react-i18next';
import { Button } from 'ui-design-system';
import { Icon } from 'ui-icons';
import * as z from 'zod';

export const handle = {
i18n: ['common', 'navigation', ...decisionsI18n] satisfies Namespace,
Expand All @@ -38,15 +42,22 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
const { apiClient } = await authService.isAuthenticated(request, {
failureRedirect: getRoute('/sign-in'),
});
const parsedParam = await parseParamsSafe(
params,
z.object({ decisionId: shortUUIDSchema }),
);
if (!parsedParam.success) {
return handleParseParamError(request, parsedParam.error);
}
const { decisionId } = parsedParam.data;

const decisionId = fromParams(params, 'decisionId');
try {
const decision = await apiClient.getDecision(decisionId);

return json({ decision });
} catch (error) {
if (isNotFoundHttpError(error)) {
throw new Response(null, { status: 404, statusText: 'Not Found' });
return notFound(null);
} else {
throw error;
}
Expand Down
28 changes: 28 additions & 0 deletions packages/app-builder/src/utils/http/handle-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { redirect } from '@remix-run/node';
import { type ZodError, type ZodIssueOptionalMessage } from 'zod';

import { isRawUUIDIssue } from '../schema/shortUUIDSchema';
import { fromUUID } from '../short-uuid';
import { badRequest } from './http-responses';

/**
* Handles a ZodError that is thrown when parsing request parameters.
*
* If the error is due to a UUID being invalid, it will redirect to the same URL with the UUID replaced by its short form.
* Otherwise, it will return a 400 Bad Request response.
*/
export function handleParseParamError<Input>(
request: Request,
error: ZodError<Input>,
) {
const { issues } = error;
if (issues.some(isRawUUIDIssue)) {
const redirectURL = (issues as ZodIssueOptionalMessage[])
.filter(isRawUUIDIssue)
.reduce((acc, { params: { value } }) => {
return acc.replace(value, fromUUID(value));
}, request.url);
return redirect(redirectURL);
}
return badRequest(error.issues);
}
22 changes: 22 additions & 0 deletions packages/app-builder/src/utils/http/http-responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { json } from '@remix-run/node';

import {
BAD_REQUEST,
FORBIDDEN,
INTERNAL_SERVER_ERROR,
NOT_FOUND,
UNAUTHORIZED,
} from './http-status-codes';

function errorResponse<Data>(status: number) {
return (data: Data, init?: Omit<ResponseInit, 'status'>) => {
throw json(data, { status, ...init });
};
}

export const badRequest = errorResponse(BAD_REQUEST);
export const unauthorized = errorResponse(UNAUTHORIZED);
export const forbidden = errorResponse(FORBIDDEN);
export const notFound = errorResponse(NOT_FOUND);

export const internalServerError = errorResponse(INTERNAL_SERVER_ERROR);
Loading

0 comments on commit 6a24940

Please sign in to comment.