Skip to content

Commit

Permalink
feat(search): show error reasons
Browse files Browse the repository at this point in the history
  • Loading branch information
yoshinorin committed Jul 2, 2024
1 parent 7f3cc36 commit 65a6449
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 97 deletions.
134 changes: 73 additions & 61 deletions src/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
fetchFromApi,
requestHeaderFrom
} from "../../api/request";
import { SearchResponse, SearchResponseWithCount } from "../../models/models";
import {
SearchResponse,
SearchResponseWithCount,
SearchSuccessResult
} from "../../models/models";
import { ProblemDetails, isProblemDetails } from "../../models/problemDetails";
import { requestContextFrom } from "../../utils/requestContext";
import { buildQueryParams, buildUrl, sluggize } from "../../utils/url";
import { Renderer } from "./renderer";
Expand All @@ -18,65 +23,68 @@ const emptyResult = {
} as SearchResponseWithCount;

export default async function Page(req: any) {
const { props } = await handler(req);
return <Renderer {...props} />;
if (req.searchParams["q"] === undefined) {
const err = {
title: "Unknown Error",
status: 422,
detail: "Unknown Error",
instance: "Unknown Error",
errors: [] as Array<string>
} as ProblemDetails;
return <Renderer props={err} qs={[]} />;
}

const qs =
req.searchParams["q"] instanceof Array
? req.searchParams["q"]
: [req.searchParams["q"]];

const result: SearchSuccessResult | ProblemDetails = await handler(req, qs);
return <Renderer props={result} qs={qs} />;
}

async function handler(req: any) {
async function handler(
req: any,
qs: Array<string>
): Promise<SearchSuccessResult | ProblemDetails> {
// TODO: refactor
// TODO: assert query params before POST to server
try {
if (req.searchParams["q"] === undefined) {
return {
props: {
statusCode: req.res.statusCode,
hits: 0,
count: 0,
contents: [],
queryStrings: []
}
};
}
const qs =
req.searchParams["q"] instanceof Array
? req.searchParams["q"]
: [req.searchParams["q"]];
if (qs.length > 0) {
const result = await execute(req, qs);
return {
props: {
statusCode: 422, // TODO
hits: result.count,
count: result.contents.length,
contents: result.contents,
queryStrings: qs
}
};
} else {
const result: SearchResponseWithCount | ProblemDetails = await execute(
req,
qs
);
if (isProblemDetails(result)) {
return {
props: {
statusCode: 422, // TODO
hits: 0,
count: 0,
contents: [],
queryStrings: qs
}
title: result.title,
status: result.status,
detail: result.detail,
instance: result.instance,
errors: result.errors
};
}
} catch {
return {
props: {
statusCode: 422, // TODO
hits: 0,
count: 0,
contents: [],
queryStrings: []
}
};
statusCode: 200,
hits: result.count,
count: result.contents.length,
contents: result.contents,
queryStrings: qs
} as SearchSuccessResult;
} catch (e) {
return {
title: "Unknown Error",
status: 500,
detail: "Unknown Error",
instance: "Unknown Error",
errors: [] as Array<string>
} as ProblemDetails;
}
}

async function execute(req, words: Array<string>) {
async function execute(
req,
words: Array<string>
): Promise<SearchResponseWithCount | ProblemDetails> {
// TODO: devide into another `function` and move `api` dir.
const url = buildUrl(api.url, sluggize(["v1", "search"]), false);
const ctx = requestContextFrom(headers());
Expand All @@ -86,28 +94,32 @@ async function execute(req, words: Array<string>) {
params: { key: "q", values: words }
})
};

const response = await fetchFromApi(url, options);
const responseBody = await response.json();

if (response.status !== 200) {
return emptyResult;
return responseBody as ProblemDetails;
}
const sr = (await response.json()) as SearchResponseWithCount;
if (sr.count === 0) {

const searchResponseWithCount = responseBody as SearchResponseWithCount;
if (searchResponseWithCount.count === 0) {
return emptyResult;
}
let contents = [];
contents = sr.contents.map((content) => {
return {
path: content.path,
title: content.title,
content: content.content,
publishedAt: content.publishedAt
} as SearchResponse;
});
contents = (searchResponseWithCount as SearchResponseWithCount).contents.map(
(content) => {
return {
path: content.path,
title: content.title,
content: content.content,
publishedAt: content.publishedAt
} as SearchResponse;
}
);

return {
count: sr.count,
count: searchResponseWithCount.count,
contents: contents
} as SearchResponseWithCount;
// TODO: Error handling
}
82 changes: 46 additions & 36 deletions src/app/search/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,46 @@ import {
CoverComponent,
SearchResultComponent
} from "../../components/components";
import { SearchResponse, SearchResponseWithCount } from "../../models/models";
import { SearchSuccessResult } from "../../models/models";
import { ProblemDetails, isProblemDetails } from "../../models/problemDetails";
import containerStyles from "../../styles/components/container.module.scss";

export const Renderer: React.FunctionComponent<{
hits: number;
count: number;
contents: Array<SearchResponse>;
queryStrings: Array<string>;
}> = ({ hits, count, contents, queryStrings }) => {
let contentsWithCount: SearchResponseWithCount = {
count: 0,
contents: []
props: SearchSuccessResult | ProblemDetails;
qs: Array<any>;
}> = ({ props, qs }) => {
let result: SearchSuccessResult = {
statusCode: 200,
queryStrings: [],
contents: [],
hits: 0,
count: 0
};

if (!isProblemDetails(props)) {
const s = props as SearchSuccessResult;
result.queryStrings = qs;
result.contents = s.contents;
result.hits = s.hits;
result.count = s.count;
}

const router = useRouter();
const [searchWord, setSearchWord] = useState(queryStrings.join(" "));
const [searchResults, setSearchResults] = useState(contents);
const [searchWord, setSearchWord] = useState(result.queryStrings.join(" "));
const [searchResults, setSearchResults] = useState(result.contents);

useEffect(() => {
if (searchWord === "") {
setSearchResults(contentsWithCount.contents);
setSearchResults(result.contents);
return;
}

const searchKeywords = searchWord.trim().toLowerCase();
if (searchKeywords.length == 0) {
setSearchResults(contentsWithCount.contents);
setSearchResults(result.contents);
return;
}
setSearchResults(contents);
setSearchResults(result.contents);
}, [searchWord]);

return (
Expand All @@ -48,25 +59,24 @@ export const Renderer: React.FunctionComponent<{
/>
<main>
<section className={`${containerStyles.container}`}>
<div className="alert warning">
<p>
<strong>NOTE:</strong>
The search feature is <strong>WIP</strong> and it has some
undocumented limitations. For more details please see belows.
</p>
<ul>
<li>
<a href="https://github.com/yoshinorin/qualtet/blob/27778232dac650153393a10dacfbc2ae62f36ac3/src/main/scala/net/yoshinorin/qualtet/domains/search/SearchService.scala#L33-L44">
Validation
</a>
</li>
<li>
<a href="https://github.com/yoshinorin/qualtet/blob/27778232dac650153393a10dacfbc2ae62f36ac3/src/main/scala/net/yoshinorin/qualtet/syntax/string.scala#L6">
Invalid Chars
</a>
</li>
</ul>
</div>
{(() => {
if (isProblemDetails(props)) {
const messages = [];
props.errors.forEach((e, idx) => {
// @ts-ignore
messages.push(<li key={idx}>{e.message}</li>);
});
return (
<div className="alert warning">
<p>
<strong>ERROR</strong>
</p>
<ul>{messages}</ul>
</div>
);
}
})()}

<form
onSubmit={(e) => {
e.preventDefault();
Expand All @@ -86,9 +96,9 @@ export const Renderer: React.FunctionComponent<{
/>
</form>
<SearchResultComponent
hits={hits}
count={count}
contents={contents}
hits={result.hits}
count={result.count}
contents={result.contents}
/>
</section>
</main>
Expand Down
26 changes: 26 additions & 0 deletions src/models/problemDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// https://datatracker.ietf.org/doc/html/rfc7807
export interface ProblemDetails {
title: string;
status: number;
detail: string;
instance: string;
errors: [code: string, message: string];
}

// TODO: Write test code
export function isProblemDetails(value: unknown): value is ProblemDetails {
if (typeof value !== "object" || value === null) {
return false;
}

const { status, errors } = value as Record<keyof ProblemDetails, unknown>;
if (typeof status !== "number") {
return false;
}

if (Array.isArray(errors)) {
return true;
}

return false;
}
8 changes: 8 additions & 0 deletions src/models/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ export interface SearchResponseWithCount {
count: number;
contents: Array<SearchResponse>;
}

export interface SearchSuccessResult {
statusCode: number;
hits: number;
count: number;
contents: Array<SearchResponse>;
queryStrings: Array<string>;
}

0 comments on commit 65a6449

Please sign in to comment.