Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/search-builder-r18.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ export default class SearchBuilderR18<
* 出力する項目を個別に指定します (of)。
* 未指定時は全項目出力されます。転送量軽減のため、このパラメータの使用が推奨されます。
* @param fields 出力するR18フィールド名、またはR18フィールド名の配列
* @return {SearchBuilderR18<SearchResultR18Fields<R18Fields>>} 型が更新されたビルダー
* @return {SearchBuilderR18<SearchResultR18Fields<TFields>, TOpt>} 型が更新されたビルダー
*/
fields<TFields extends R18Fields>(
fields: TFields | readonly TFields[]
): SearchBuilderR18<SearchResultR18Fields<R18Fields>> {
): SearchBuilderR18<SearchResultR18Fields<TFields>, TOpt> {
this.set({ of: NovelSearchBuilderBase.array2string(fields) });
// oxlint-disable-next-line typescript/no-explicit-any
return this as any;
Expand Down
229 changes: 229 additions & 0 deletions test/builder-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { describe, expectTypeOf, test } from "vitest";
import NarouAPI, {
Fields,
OptionalFields,
R18Fields,
UserFields,
} from "../src/index.js";
import NarouBrowserAPI from "../src/index.browser.js";
import type {
DefaultSearchResultFields,
NarouRankingResult,
NarouSearchResult,
UserSearchResult,
} from "../src/index.js";

// executeは呼び出さず、tsc --noEmitで公開APIの型を検証する。
describe.each([
{ name: "Node", api: NarouAPI },
{ name: "browser", api: NarouBrowserAPI },
])("$name builder types", ({ api }) => {
test("通常検索のデフォルト項目", () => {
const builder = api.search();

expectTypeOf<
Awaited<ReturnType<typeof builder.execute>>["values"]
>().toEqualTypeOf<
readonly Pick<NarouSearchResult, DefaultSearchResultFields>[]
>();
});

test("通常検索の単一項目とreadonly配列", () => {
const single = api.search().fields(Fields.title);
const multiple = api.search().fields([Fields.title, Fields.ncode] as const);

expectTypeOf<
Awaited<ReturnType<typeof single.execute>>["values"]
>().toEqualTypeOf<readonly { title: string }[]>();
expectTypeOf<
Awaited<ReturnType<typeof multiple.execute>>["values"]
>().toEqualTypeOf<readonly { title: string; ncode: string }[]>();
});

test("項目未指定時はnovel_typeを返す", () => {
const novel = api.search();
const r18 = api.searchR18();

expectTypeOf<Awaited<ReturnType<typeof novel.execute>>["values"][number]>()
.toHaveProperty("novel_type")
.toEqualTypeOf<1 | 2>();
expectTypeOf<
Awaited<ReturnType<typeof novel.execute>>["values"][number]
>().not.toHaveProperty("noveltype");
expectTypeOf<Awaited<ReturnType<typeof r18.execute>>["values"][number]>()
.toHaveProperty("novel_type")
.toEqualTypeOf<1 | 2>();
expectTypeOf<
Awaited<ReturnType<typeof r18.execute>>["values"][number]
>().not.toHaveProperty("noveltype");
});

test("of=nt指定時はnovel_typeではなくnoveltypeを返す", () => {
const novel = api.search().fields(Fields.noveltype);
const r18 = api.searchR18().fields(R18Fields.noveltype);

expectTypeOf<
Awaited<ReturnType<typeof novel.execute>>["values"]
>().toEqualTypeOf<readonly { noveltype: 1 | 2 }[]>();
expectTypeOf<
Awaited<ReturnType<typeof r18.execute>>["values"]
>().toEqualTypeOf<readonly { noveltype: 1 | 2 }[]>();
});

test("通常検索のoptとfieldsの順序に依存しない型推論", () => {
const optFirst = api
.search()
.opt(OptionalFields.weekly_unique)
.fields(Fields.title);
const fieldsFirst = api
.search()
.fields(Fields.title)
.opt([OptionalFields.weekly_unique] as const);

expectTypeOf<
Awaited<ReturnType<typeof optFirst.execute>>["values"]
>().toEqualTypeOf<readonly { title: string; weekly_unique: number }[]>();
expectTypeOf<
Awaited<ReturnType<typeof fieldsFirst.execute>>["values"]
>().toEqualTypeOf<readonly { title: string; weekly_unique: number }[]>();
});

test("通常検索の条件チェーンと取得項目の再指定", () => {
const builder = api
.search()
.fields([Fields.title, Fields.ncode])
.opt(OptionalFields.weekly_unique)
.word("test")
.length({ min: 100 })
.page(1, 10)
.fields(Fields.title);

expectTypeOf<
Awaited<ReturnType<typeof builder.execute>>["values"]
>().toEqualTypeOf<readonly { title: string; weekly_unique: number }[]>();
});

test("R18検索の専用項目と条件チェーン", () => {
const builder = api
.searchR18()
.fields([R18Fields.title, R18Fields.nocgenre] as const)
.opt(OptionalFields.weekly_unique)
.xid([1, 2])
.word("test")
.limit(10);

expectTypeOf<
Awaited<ReturnType<typeof builder.execute>>["values"]
>().toEqualTypeOf<
readonly {
title: string;
nocgenre: NarouSearchResult["nocgenre"];
weekly_unique: number;
}[]
>();
});

test("ユーザー検索のデフォルト項目", () => {
const builder = api.searchUser();

expectTypeOf<
Awaited<ReturnType<typeof builder.execute>>["values"]
>().toEqualTypeOf<readonly UserSearchResult[]>();
});

test("ユーザー検索の単一項目とreadonly配列", () => {
const single = api.searchUser().fields(UserFields.name);
const multiple = api
.searchUser()
.fields([UserFields.name, UserFields.userid] as const);

expectTypeOf<
Awaited<ReturnType<typeof single.execute>>["values"]
>().toEqualTypeOf<readonly { name: string }[]>();
expectTypeOf<
Awaited<ReturnType<typeof multiple.execute>>["values"]
>().toEqualTypeOf<readonly { name: string; userid: number }[]>();
});

test("ユーザー検索の条件チェーンと取得項目の再指定", () => {
const builder = api
.searchUser()
.fields([UserFields.name, UserFields.userid])
.word("test")
.minNovel(1)
.page(1, 10)
.fields(UserFields.userid);

expectTypeOf<
Awaited<ReturnType<typeof builder.execute>>["values"]
>().toEqualTypeOf<readonly { userid: number }[]>();
});

test("ランキングの基本結果とデフォルトの詳細項目", () => {
const builder = api.ranking();

expectTypeOf(builder.execute).returns.toEqualTypeOf<
Promise<NarouRankingResult[]>
>();
expectTypeOf(() => builder.executeWithFields()).returns.toEqualTypeOf<
Promise<
(NarouRankingResult &
Partial<Pick<NarouSearchResult, DefaultSearchResultFields>>)[]
>
>();
});

test("ランキングの取得項目の指定とオプションの組み合わせ", () => {
const builder = api.ranking();

expectTypeOf(() =>
builder.executeWithFields(Fields.title)
).returns.toEqualTypeOf<
Promise<(NarouRankingResult & { title?: string })[]>
>();
expectTypeOf(() =>
builder.executeWithFields(
[Fields.title, Fields.writer],
[OptionalFields.weekly_unique]
)
).returns.toEqualTypeOf<
Promise<
(NarouRankingResult & {
title?: string;
writer?: string;
weekly_unique?: number;
})[]
>
>();
expectTypeOf(() =>
builder.executeWithFields([], OptionalFields.weekly_unique)
).returns.toEqualTypeOf<
Promise<
(NarouRankingResult &
Partial<
Pick<NarouSearchResult, DefaultSearchResultFields | "weekly_unique">
>)[]
>
>();
});

test("未取得の項目と他API専用のフィールドを型で拒否", () => {
// この関数は実行せず、@ts-expect-errorが有効かを型チェックする。
function invalidUsage() {
// @ts-expect-error 通常検索ではR18専用のフィールドを指定できない
api.search().fields(R18Fields.nocgenre);
// @ts-expect-error R18検索では通常検索専用のフィールドを指定できない
api.searchR18().fields(Fields.genre);
// @ts-expect-error ユーザー検索では小説タイトルを指定できない
api.searchUser().fields(Fields.title);

const result = api.search().fields(Fields.title).execute();
void result.then(({ values }) => {
// @ts-expect-error 未取得のncodeは参照できない
return values[0].ncode;
});
}

expectTypeOf(invalidUsage).returns.toBeVoid();
});
});
23 changes: 23 additions & 0 deletions test/narou.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ describe("narou-test", () => {
expect(result.values).toHaveLength(1);
});
});
describe.each(["search", "searchR18"] as const)(
"%s novel_typeとnoveltypeの出力名(実API)",
(searchMethod) => {
it("項目未指定時はnovel_typeのみを返す", async () => {
const result = await NarouAPI[searchMethod]().limit(1).execute();

expect(result.values).toHaveLength(1);
expect([1, 2]).toContain(result.values[0].novel_type);
expect(result.values[0]).not.toHaveProperty("noveltype");
});

it("of=nt指定時はnoveltypeのみを返す", async () => {
const builder = searchMethod === "search"
? NarouAPI.search().fields(Fields.noveltype)
: NarouAPI.searchR18().fields(R18Fields.noveltype);
const result = await builder.limit(1).execute();

expect(result.values).toHaveLength(1);
expect([1, 2]).toContain(result.values[0].noveltype);
expect(Object.keys(result.values[0])).toEqual(["noveltype"]);
});
}
);
describe.each(["search", "searchR18"] as const)(
"%s 日付範囲の片側省略(実API)",
(searchMethod) => {
Expand Down
Loading
Loading