feat: compose TechLog static and mock adapters
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
projects,
|
||||
publicRecords,
|
||||
releases,
|
||||
type Project,
|
||||
type ProjectActivity,
|
||||
type ProjectDecision,
|
||||
type PublicRecord,
|
||||
type RecordKind,
|
||||
type Release,
|
||||
type HomeFocusItem,
|
||||
} from "./public-content.ts";
|
||||
import type { PublicContentQueries } from "../../application/ports/public-content-queries.ts";
|
||||
|
||||
export type RecordFilters = {
|
||||
kind?: RecordKind;
|
||||
topic?: string;
|
||||
project?: string;
|
||||
openQuestionsOnly?: boolean;
|
||||
};
|
||||
|
||||
export type SearchablePublicEntity = {
|
||||
contentType: RecordKind | "PROJECT" | "RELEASE";
|
||||
title: string;
|
||||
summary: string;
|
||||
path: string;
|
||||
topic?: string;
|
||||
topics?: ReadonlyArray<string>;
|
||||
project?: string;
|
||||
publishedAt?: string;
|
||||
};
|
||||
|
||||
function comparePublishedAt(
|
||||
left: Pick<PublicRecord, "publishedAt">,
|
||||
right: Pick<PublicRecord, "publishedAt">,
|
||||
) {
|
||||
return right.publishedAt.localeCompare(left.publishedAt);
|
||||
}
|
||||
|
||||
export function listRecords(filters: RecordFilters = {}): PublicRecord[] {
|
||||
const hasTopicFilter = filters.topic !== undefined;
|
||||
const hasProjectFilter = filters.project !== undefined;
|
||||
const requestedTopic = filters.topic?.trim().toLocaleLowerCase("ko-KR");
|
||||
const requestedProject = filters.project
|
||||
?.trim()
|
||||
.toLocaleLowerCase("ko-KR");
|
||||
|
||||
return publicRecords
|
||||
.filter((record) => !filters.kind || record.kind === filters.kind)
|
||||
.filter(
|
||||
(record) =>
|
||||
!hasTopicFilter ||
|
||||
record.topic.toLocaleLowerCase("ko-KR") === requestedTopic,
|
||||
)
|
||||
.filter(
|
||||
(record) =>
|
||||
!hasProjectFilter ||
|
||||
record.projectSlug.toLocaleLowerCase("ko-KR") === requestedProject ||
|
||||
record.projectTitle.toLocaleLowerCase("ko-KR") === requestedProject,
|
||||
)
|
||||
.filter(
|
||||
(record) =>
|
||||
!filters.openQuestionsOnly ||
|
||||
(record.kind === "QUESTION" &&
|
||||
record.questionStatus !== "RESOLVED" &&
|
||||
record.questionStatus !== "ARCHIVED"),
|
||||
)
|
||||
.sort(comparePublishedAt);
|
||||
}
|
||||
|
||||
export function getRecord<K extends RecordKind>(
|
||||
kind: K,
|
||||
slug: string,
|
||||
): Extract<PublicRecord, { kind: K }> | undefined {
|
||||
return publicRecords.find(
|
||||
(record) => record.kind === kind && record.slug === slug,
|
||||
) as Extract<PublicRecord, { kind: K }> | undefined;
|
||||
}
|
||||
|
||||
export function getProject(slug: string): Project | undefined {
|
||||
return projects.find((project) => project.slug === slug);
|
||||
}
|
||||
|
||||
export function getRelease(version: string): Release | undefined {
|
||||
return releases.find((release) => release.version === version);
|
||||
}
|
||||
|
||||
export function getProjectRecords(projectSlug: string): PublicRecord[] {
|
||||
if (!getProject(projectSlug)) return [];
|
||||
return listRecords({ project: projectSlug });
|
||||
}
|
||||
|
||||
export function getProjectDecisions(projectSlug: string): ProjectDecision[] {
|
||||
return [...(getProject(projectSlug)?.decisions ?? [])];
|
||||
}
|
||||
|
||||
export function getProjectActivity(projectSlug: string): ProjectActivity[] {
|
||||
return [...(getProject(projectSlug)?.activity ?? [])];
|
||||
}
|
||||
|
||||
export function getHomeFocusItems(): HomeFocusItem[] {
|
||||
const project = getProject("backend-skeleton");
|
||||
const question = getRecord("QUESTION", "validate-edge-token-again");
|
||||
const decision = getProjectDecisions("backend-skeleton").find(
|
||||
(item) => item.id === "storage-port-unification",
|
||||
);
|
||||
|
||||
if (!project || !question || !decision) {
|
||||
throw new Error("Missing canonical entity required for the public home focus");
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: "current",
|
||||
label: "현재 작업",
|
||||
title: project.title,
|
||||
summary: project.summary,
|
||||
details: [
|
||||
{ label: "단계", value: project.stage },
|
||||
{ label: "현재 목표", value: project.currentGoal },
|
||||
{ label: "다음 작업", value: project.nextStep },
|
||||
],
|
||||
targetPath: `/projects/${project.slug}`,
|
||||
},
|
||||
{
|
||||
key: "question",
|
||||
label: "열린 질문",
|
||||
title: question.title,
|
||||
summary: question.summary,
|
||||
details: [
|
||||
{ label: "확인한 사실", value: question.facts[0] ?? "" },
|
||||
{ label: "남은 미지수", value: question.unknowns[0] ?? "" },
|
||||
{ label: "다음 검증", value: question.nextValidation },
|
||||
],
|
||||
targetPath: question.path,
|
||||
},
|
||||
{
|
||||
key: "decision",
|
||||
label: "최근 결정",
|
||||
title: decision.statement,
|
||||
summary: decision.rationale,
|
||||
details: [
|
||||
{ label: "영향", value: decision.consequences[0] ?? "" },
|
||||
{ label: "근거", value: decision.evidence[0]?.title ?? "" },
|
||||
],
|
||||
targetPath: `/projects/${project.slug}/decisions#${decision.id}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function recordSearchEntity(record: PublicRecord): SearchablePublicEntity {
|
||||
return {
|
||||
contentType: record.kind,
|
||||
title: record.title,
|
||||
summary: record.summary,
|
||||
path: record.path,
|
||||
topic: record.topic,
|
||||
project: record.projectTitle,
|
||||
publishedAt: record.publishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function projectSearchEntity(project: Project): SearchablePublicEntity {
|
||||
return {
|
||||
contentType: "PROJECT",
|
||||
title: project.title,
|
||||
summary: project.summary,
|
||||
path: `/projects/${project.slug}`,
|
||||
topics: project.topics,
|
||||
project: project.title,
|
||||
};
|
||||
}
|
||||
|
||||
function releaseSearchEntity(release: Release): SearchablePublicEntity {
|
||||
return {
|
||||
contentType: "RELEASE",
|
||||
title: release.title,
|
||||
summary: release.summary,
|
||||
path: release.path,
|
||||
topic: "TechLog",
|
||||
project: "TechLog",
|
||||
publishedAt: release.publishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function searchPublicContent(query: string): SearchablePublicEntity[] {
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase("ko-KR");
|
||||
const entities = [
|
||||
...publicRecords.map(recordSearchEntity),
|
||||
...projects.map(projectSearchEntity),
|
||||
...releases.map(releaseSearchEntity),
|
||||
];
|
||||
|
||||
if (!normalizedQuery) return entities;
|
||||
|
||||
return entities.filter((entity) =>
|
||||
[
|
||||
entity.title,
|
||||
entity.summary,
|
||||
entity.topic,
|
||||
entity.project,
|
||||
...(entity.topics ?? []),
|
||||
]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.some((value) =>
|
||||
value.toLocaleLowerCase("ko-KR").includes(normalizedQuery),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export const publicContentQueries = Object.freeze({
|
||||
listRecords,
|
||||
getRecord,
|
||||
getProject,
|
||||
getRelease,
|
||||
getProjectRecords,
|
||||
getProjectDecisions,
|
||||
getProjectActivity,
|
||||
getHomeFocusItems,
|
||||
searchPublicContent,
|
||||
}) satisfies PublicContentQueries;
|
||||
Reference in New Issue
Block a user