/**
* エラー内容の選択肢(`error_lists`)の読み取りと、エラー行(`robot_error_reports.error_list_id`)の名前解決(指示書BD §5-3。D-184)。
*
* ★2つの用途で規則が違う★(指示書BD §3-1 / §3-2):
* - 選択肢(`GET /error-lists`)= ★有効な行だけ★(`inactive = 0` かつ `deleted_at IS NULL`)・★id の昇順★
* → `activeErrorListItems()`(★これが正★。Prisma 版の `ACTIVE_ERROR_LIST_WHERE` / `orderBy` は写し)
* - 名前解決(`ErrorReportItem.error_list`)= ★絞らない★(無効化・削除済みの行でも解決する。施設名と同じ。罠7)
* → `errorListIndex()` / `resolveErrorList()`。★`loadErrorListIndexByIds()` に `inactive` / `deleted_at` の条件を足さないこと★
*
* ★N+1 にしない★ — 一覧・1件とも、行の `error_list_id`(非 null)を `errorListIdsOf()` で集めて
* ★`errorList.findMany({ where: { id: { in } } })` を1回★だけ撃つ(id が0件なら撃たない)。
* ★`in` の要素数は `error_lists` の行数(決定稿 131行)を超えない★(同じ id は1つに畳む)ので分割しない。
*
* ★文字列は DB の値のまま★(`t()` に通さない・変換しない。D-077 と同じ。指示書BD §2)。
* ★BigInt → number は `Number()`★(`facility_id` 等と同じ。`lib/db/error-report-mapping.ts`)。
* ★Map のキーは `bigint.toString()`★(`bigint` は Map のキーにならない。予定の引き当てと同じ。指示書AW 4-9)。
*
* テストは `lib/db/__tests__/error-list-source.test.ts`。
*/
import { ERROR_LIST_NOTE_REQUIRED_IDS, ERROR_LIST_UNSELECTABLE_ERROR_TYPES } from "@/lib/config";
import type { Prisma } from "@/lib/generated/prisma/client";
import type { ErrorListItem } from "@/lib/types";
/**
* `error_lists` から読む列(選択肢・名前解決の共通)。
* ★`inactive` / `deleted_at` は `activeErrorListItems()`(選択肢の絞り込みの正)が見る★。名前解決では見ない。
* ★`created_at` / `updated_at` は読まない★(API に出さない)。
*/
export const ERROR_LIST_SELECT = {
id: true,
error_category_main_en: true,
error_category_main_ja: true,
error_category_sub_en: true,
error_category_sub_ja: true,
error_detail_en: true,
error_detail_ja: true,
inactive: true,
deleted_at: true,
} as const satisfies Prisma.ErrorListSelect;
/** `ERROR_LIST_SELECT` の結果の形(fixtures の `FixtureErrorListRow` もこれを満たす) */
export interface ErrorListRowSource {
id: bigint;
error_category_main_en: string;
error_category_main_ja: string;
error_category_sub_en: string;
error_category_sub_ja: string;
error_detail_en: string;
error_detail_ja: string;
inactive: number;
deleted_at: Date | null;
}
/**
* 選択肢の `where`(★SQL 側の写し。正は `isActiveErrorListRow()`★)。
* ★名前解決(`loadErrorListIndexByIds()`)には使わない★。
*/
export const ACTIVE_ERROR_LIST_WHERE = {
inactive: 0,
deleted_at: null,
} as const satisfies Prisma.ErrorListWhereInput;
/** 選択肢に出す行か(`inactive === 0` かつ `deleted_at === null`。★`inactive` は 0 以外をすべて無効とみなす★) */
export function isActiveErrorListRow(row: Pick<ErrorListRowSource, "inactive" | "deleted_at">): boolean {
return row.inactive === 0 && row.deleted_at === null;
}
/** 行 → `ErrorListItem`(★文字列は変換しない★。`note_required` は `ERROR_LIST_NOTE_REQUIRED_IDS` から) */
export function errorListItemFromRow(row: ErrorListRowSource): ErrorListItem {
const id = Number(row.id);
return {
id,
error_category_main_en: row.error_category_main_en,
error_category_main_ja: row.error_category_main_ja,
error_category_sub_en: row.error_category_sub_en,
error_category_sub_ja: row.error_category_sub_ja,
error_detail_en: row.error_detail_en,
error_detail_ja: row.error_detail_ja,
note_required: ERROR_LIST_NOTE_REQUIRED_IDS.includes(id),
};
}
/**
* 選択肢(`GET /error-lists`): ★有効な行だけ・id の昇順★(fixtures 版・Prisma 版で共有。★これが正★)。
* ★入力の配列を並べ替えない★(コピーしてから並べる)。
*/
export function activeErrorListItems(rows: readonly ErrorListRowSource[]): ErrorListItem[] {
return rows
.filter(isActiveErrorListRow)
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
.map(errorListItemFromRow);
}
/** 名前解決の索引(キーは `error_lists.id` の `toString()`) */
export type ErrorListIndex = ReadonlyMap<string, ErrorListItem>;
/** 空の索引(`error_list_id` が全部 null のとき。★クエリを撃たない★) */
export const EMPTY_ERROR_LIST_INDEX: ErrorListIndex = new Map();
/** ★絞らずに★索引を作る(無効化・削除済みの行も入れる。名前解決は行の有無だけを見る) */
export function errorListIndex(rows: readonly ErrorListRowSource[]): ErrorListIndex {
return new Map(rows.map((row) => [row.id.toString(), errorListItemFromRow(row)]));
}
/** エラー行の `error_list_id` のうち非 null のものを、重複なく集める(`in` に渡す。並びは初出順) */
export function errorListIdsOf(rows: readonly { error_list_id: bigint | null }[]): bigint[] {
const seen = new Map<string, bigint>();
for (const row of rows) {
if (row.error_list_id !== null) seen.set(row.error_list_id.toString(), row.error_list_id);
}
return [...seen.values()];
}
/**
* `error_list_id` → `ErrorListItem | null`。
* ★null(未選択)→ null★/★索引に無い id(`error_lists` に無い)→ null★(画面は後者を「不明(id)」と出す。`error_list_id` は値を保つ)。
*/
export function resolveErrorList(errorListId: bigint | null, index: ErrorListIndex): ErrorListItem | null {
if (errorListId === null) return null;
return index.get(errorListId.toString()) ?? null;
}
/**
* この行でエラー内容を選べるか(`ErrorReportItem.error_list_selectable`。指示書BD §3-2)。
* ★`ERROR_LIST_UNSELECTABLE_ERROR_TYPES` との完全一致(`===`)★ — 大小文字・前後の空白を緩めない
* (`D_SEC_ALERT_ERROR_TYPES` と同じ。緩めると、選べるはずの行が選べなくなる/その逆が静かに起きる)。
* ★`PATCH /errors/{error_id}/status` の検証(指示書BD §3-3 手順2)も、行の `error_list_selectable` を通してこの1本を使う★。
*/
export function isErrorListSelectable(errorType: string): boolean {
return !ERROR_LIST_UNSELECTABLE_ERROR_TYPES.includes(errorType);
}
/**
* この関数が必要とする Prisma の最小限の形(★テストからスタブを渡せるようにするため★。`facility-name-source.ts` と同じ作法)。
*/
export interface ErrorListSourcePrisma {
errorList: {
findMany(args: {
where: { id: { in: bigint[] } };
select: typeof ERROR_LIST_SELECT;
}): Promise<ErrorListRowSource[]>;
};
}
/**
* 名前解決の索引を Prisma から作る(一覧・1件で共有)。
* ★`where` は `id IN` だけ★(`inactive` / `deleted_at` で絞らない。罠7と同じ。`lib/db/__tests__/error-list-source.test.ts` が固定)。
* ★id が0件ならクエリを撃たない★。
*/
export async function loadErrorListIndexByIds(
prisma: ErrorListSourcePrisma,
ids: readonly bigint[]
): Promise<ErrorListIndex> {
if (ids.length === 0) return EMPTY_ERROR_LIST_INDEX;
const rows = await prisma.errorList.findMany({
where: { id: { in: [...ids] } },
select: ERROR_LIST_SELECT,
});
return errorListIndex(rows);
}