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
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type NarouNovel from "./narou.js";
import type { ExecuteOptions } from "./narou.js";
import NarouNovelFetch from "./narou-fetch.js";
import NarouNovelJsonp from "./narou-jsonp.js";
import RankingBuilder from "./ranking.js";
Expand Down Expand Up @@ -67,13 +68,16 @@ export function ranking(api: NarouNovel = narouNovelFetch): RankingBuilder {
/**
* なろう殿堂入り API でランキング履歴を取得する
* @param {string} ncode 小説のNコード
* @param {ExecuteOptions} [options] 実行オプション
* @param {NarouNovel} [api] API実行クラスのインスタンス
* @see https://dev.syosetu.com/man/rankinapi/
*/
export async function rankingHistory(
ncode: string,
options?: ExecuteOptions,
api: NarouNovel = narouNovelFetch
): Promise<RankingHistoryResult[]> {
Comment on lines 75 to 79
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter order places options before api, which is inconsistent with common JavaScript/TypeScript patterns where optional configuration parameters typically come last. Consider reordering parameters to (ncode: string, api?: NarouNovel, options?: ExecuteOptions) or making options the last parameter as (ncode: string, options?: ExecuteOptions, api?: NarouNovel). However, placing the optional override parameter api last is more conventional, so the recommended signature would be: (ncode: string, options?: ExecuteOptions, api?: NarouNovel).

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これはAPIクライアントをできるだけ隠蔽したいのでこのまま

Comment on lines 75 to 79

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge rankingHistory now ignores custom API argument

The new rankingHistory signature adds options ahead of the api parameter, so a call like rankingHistory("n0001", narouNovelJsonp)—previously supported—now treats the API instance as ExecuteOptions and silently falls back to the default fetch implementation. This breaks existing callers that supply a custom NarouNovel (e.g., the JSONP version) and prevents those instances from being used without changing call sites.

Useful? React with 👍 / 👎.

const result = await api.executeRankingHistory({ ncode });
const result = await api.executeRankingHistory({ ncode }, options);
Comment on lines 75 to +80

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve custom API parameter order in rankingHistory

The public rankingHistory helper now inserts options before api, but still defaults the third argument to narouNovelFetch. Any existing calls that pass a custom NarouNovel as the second parameter (the previous signature) will now be treated as ExecuteOptions, so the custom client is ignored and the default fetch client is used instead, with the misplaced options object flowing into executeRankingHistory. This is a backwards-incompatible regression for consumers needing a custom transport (e.g., JSONP or stubbed fetch) and can cause incorrect network paths or runtime errors.

Useful? React with 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIを破壊的な変更してるから次のリリースは2.0.0かな…

if (Array.isArray(result)) {
return result.map(formatRankingHistory);
} else {
Expand Down
7 changes: 4 additions & 3 deletions src/narou-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { unzipp } from "./util/unzipp.js";
import NarouNovel from "./narou.js";
import type { NarouParams } from "./narou.js";
import type { NarouParams, ExecuteOptions } from "./narou.js";

type Fetch = typeof fetch;

Expand All @@ -18,7 +18,8 @@ export default class NarouNovelFetch extends NarouNovel {

protected async execute<T>(
params: NarouParams,
endpoint: string
endpoint: string,
options?: ExecuteOptions
): Promise<T> {
const query = { ...params, out: "json" };

Expand All @@ -36,7 +37,7 @@ export default class NarouNovelFetch extends NarouNovel {
}
});

const res = await (this.fetch ?? fetch)(url);
const res = await (this.fetch ?? fetch)(url, options?.fetchOptions);

if (!query.gzip) {
return (await res.json()) as T;
Expand Down
6 changes: 4 additions & 2 deletions src/narou-jsonp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import NarouNovel from "./narou.js";
import type { NarouParams } from "./narou.js";
import type { NarouParams, ExecuteOptions } from "./narou.js";
import { jsonp } from "./util/jsonp.js";

/**
Expand All @@ -8,7 +8,9 @@ import { jsonp } from "./util/jsonp.js";
export default class NarouNovelJsonp extends NarouNovel {
protected async execute<T>(
params: NarouParams,
endpoint: string
endpoint: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_options?: ExecuteOptions
): Promise<T> {
const query = { ...params, out: "jsonp" };
query.gzip = 0;
Expand Down
63 changes: 50 additions & 13 deletions src/narou.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export type NarouParams =
| RankingHistoryParams
| UserSearchParams;

/**
* なろう小説APIへのリクエストオプション
*/
export interface ExecuteOptions {
/**
* fetch関数のオプション
*/
fetchOptions?: RequestInit;
}

/**
* なろう小説APIへのリクエストを実行する
* @class NarouNovel
Expand All @@ -35,7 +45,8 @@ export default abstract class NarouNovel {
*/
protected abstract execute<T>(
params: NarouParams,
endpoint: string
endpoint: string,
options?: ExecuteOptions
): Promise<T>;

/**
Expand All @@ -46,9 +57,13 @@ export default abstract class NarouNovel {
*/
protected async executeSearch<T extends keyof NarouSearchResult>(
params: SearchParams,
endpoint = "https://api.syosetu.com/novelapi/api/"
endpoint = "https://api.syosetu.com/novelapi/api/",
options?: ExecuteOptions
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return new NarouSearchResults(await this.execute(params, endpoint), params);
return new NarouSearchResults(
await this.execute(params, endpoint, options),
params
);
}

/**
Expand All @@ -58,11 +73,13 @@ export default abstract class NarouNovel {
* @see https://dev.syosetu.com/man/api/
*/
async executeNovel<T extends keyof NarouSearchResult>(
params: SearchParams
params: SearchParams,
options?: ExecuteOptions
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return await this.executeSearch(
params,
"https://api.syosetu.com/novelapi/api/"
"https://api.syosetu.com/novelapi/api/",
options
);
}

Expand All @@ -73,11 +90,13 @@ export default abstract class NarouNovel {
* @see https://dev.syosetu.com/xman/api/
*/
async executeNovel18<T extends keyof NarouSearchResult>(
params: SearchParams
params: SearchParams,
options?: ExecuteOptions
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return await this.executeSearch(
params,
"https://api.syosetu.com/novel18api/api/"
"https://api.syosetu.com/novel18api/api/",
options
);
}

Expand All @@ -87,20 +106,33 @@ export default abstract class NarouNovel {
* @returns ランキング結果
* @see https://dev.syosetu.com/man/rankapi/
*/
async executeRanking(params: RankingParams): Promise<NarouRankingResult[]> {
return await this.execute(params, "https://api.syosetu.com/rank/rankget/");
async executeRanking(
params: RankingParams,
options?: ExecuteOptions
): Promise<NarouRankingResult[]> {
return await this.execute(
params,
"https://api.syosetu.com/rank/rankget/",
options
);
}

/**
* 殿堂入りAPiへのリクエストを実行する
* @param params クエリパラメータ
* @param options 実行オプション
* @returns ランキング履歴結果
* @see https://dev.syosetu.com/man/rankinapi/
*/
async executeRankingHistory(
params: RankingHistoryParams
params: RankingHistoryParams,
options?: ExecuteOptions
): Promise<RankingHistoryRawResult[]> {
return await this.execute(params, "https://api.syosetu.com/rank/rankin/");
return await this.execute(
params,
"https://api.syosetu.com/rank/rankin/",
options
);
}

/**
Expand All @@ -110,10 +142,15 @@ export default abstract class NarouNovel {
* @see https://dev.syosetu.com/man/userapi/
*/
async executeUserSearch<T extends keyof UserSearchResult>(
params: UserSearchParams
params: UserSearchParams,
options?: ExecuteOptions
): Promise<NarouSearchResults<UserSearchResult, T>> {
return new NarouSearchResults<UserSearchResult, T>(
await this.execute(params, "https://api.syosetu.com/userapi/api/"),
await this.execute(
params,
"https://api.syosetu.com/userapi/api/",
options
),
params
);
}
Expand Down
31 changes: 20 additions & 11 deletions src/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Fields,
} from "./params.js";
import type NarouNovel from "./narou.js";
import type { ExecuteOptions } from "./narou.js";
import type { SearchResultFields } from "./narou-search-results.js";
import { addDays, formatDate } from "./util/date.js";

Expand Down Expand Up @@ -108,21 +109,24 @@ export default class RankingBuilder {
* 設定されたパラメータに基づき、なろう小説ランキングAPIへのリクエストを実行します。
*
* 返される結果には、Nコード、ポイント、順位が含まれます。
* @param options 実行オプション
* @returns {Promise<NarouRankingResult[]>} ランキング結果の配列
* @see https://dev.syosetu.com/man/rankapi/#output
*/
execute(): Promise<NarouRankingResult[]> {
execute(options?: ExecuteOptions): Promise<NarouRankingResult[]> {
const date = formatDate(this.date$);
this.set({ rtype: `${date}-${this.type$}` });
return this.api.executeRanking(this.params as RankingParams);
return this.api.executeRanking(this.params as RankingParams, options);
}

/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
*/
async executeWithFields(): Promise<
RankingResult<DefaultSearchResultFields>[]
>;
async executeWithFields(
fields?: never[] | undefined,
opt?: never[] | undefined,
options?: ExecuteOptions
): Promise<RankingResult<DefaultSearchResultFields>[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
*
Expand All @@ -131,7 +135,9 @@ export default class RankingBuilder {
* @returns {Promise<RankingResult<SearchResultFields<TFields>>[]>} 詳細情報を含むランキング結果の配列
*/
async executeWithFields<TFields extends Fields>(
fields: TFields | TFields[]
fields: TFields | TFields[],
opt?: never | never[],
options?: ExecuteOptions
): Promise<RankingResult<SearchResultFields<TFields>>[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
Expand All @@ -141,7 +147,8 @@ export default class RankingBuilder {
*/
async executeWithFields(
fields: never[],
opt: OptionalFields | OptionalFields[]
opt: OptionalFields | OptionalFields[],
options?: ExecuteOptions
): Promise<RankingResult<DefaultSearchResultFields | "weekly_unique">[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
Expand All @@ -153,7 +160,8 @@ export default class RankingBuilder {
*/
async executeWithFields<TFields extends Fields>(
fields: TFields | TFields[],
opt: OptionalFields | OptionalFields[]
opt: OptionalFields | OptionalFields[],
options?: ExecuteOptions
): Promise<RankingResult<SearchResultFields<TFields> | "weekly_unique">[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
Expand All @@ -169,9 +177,10 @@ export default class RankingBuilder {
TOpt extends OptionalFields | undefined = undefined
>(
fields: TFields | TFields[] = [],
opt?: TOpt
opt?: TOpt,
options?: ExecuteOptions
): Promise<RankingResult<SearchResultFields<TFields>>[]> {
Comment on lines 179 to 182

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge executeWithFields drops options when no fields are provided

Overloads advertise executeWithFields(options?: ExecuteOptions), but the implementation keeps options as the third parameter. Calling the method with only execution options (the intended new use case) binds that object to fields, producing an of=[object Object]-ncode query and never forwarding fetchOptions to the API call. Options-only calls therefore malfunction unless the caller now passes placeholder field arguments or reshapes the signature.

Useful? React with 👍 / 👎.

const ranking = await this.execute();
const ranking = await this.execute(options);
const fields$ = Array.isArray(fields)
? fields.length == 0
? []
Expand All @@ -186,7 +195,7 @@ export default class RankingBuilder {
}
builder.ncode(rankingNcodes);
builder.limit(ranking.length);
const result = await builder.execute();
const result = await builder.execute(options);

return ranking.map<
RankingResult<
Expand Down
8 changes: 6 additions & 2 deletions src/search-builder-r18.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NovelSearchBuilderBase } from "./search-builder.js";
import type { ExecuteOptions } from "./narou.js";
import type NarouSearchResults from "./narou-search-results.js";
import type {
NarouSearchResult,
Expand Down Expand Up @@ -28,10 +29,13 @@ export default class SearchBuilderR18<
/**
* なろう小説APIへの検索リクエストを実行する
* @override
* @param options 実行オプション
* @returns {Promise<NarouSearchResults>} 検索結果
*/
execute(): Promise<NarouSearchResults<NarouSearchResult, T | TOpt>> {
return this.api.executeNovel18(this.params);
execute(
options?: ExecuteOptions
): Promise<NarouSearchResults<NarouSearchResult, T | TOpt>> {
return this.api.executeNovel18(this.params, options);
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/search-builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type NarouNovel from "./narou.js";
import type { ExecuteOptions } from "./narou.js";
import type {
NarouSearchResult,
SearchResultFields,
Expand Down Expand Up @@ -40,7 +41,7 @@ export abstract class SearchBuilderBase<
constructor(
protected params: TParams = {} as TParams,
protected api: NarouNovel
) {}
) { }
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor closing brace has an extra space before it. Should be } instead of } .

Suggested change
) { }
) {}

Copilot uses AI. Check for mistakes.

/**
* 配列から重複を除去する
Expand Down Expand Up @@ -472,10 +473,13 @@ export abstract class NovelSearchBuilderBase<

/**
* なろう小説APIへの検索リクエストを実行する
* @param options 実行オプション
* @returns {Promise<NarouSearchResults>} 検索結果
*/
execute(): Promise<NarouSearchResults<NarouSearchResult, T>> {
return this.api.executeNovel(this.params);
execute(
options?: ExecuteOptions
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return this.api.executeNovel(this.params, options);
}
}

Expand Down
11 changes: 9 additions & 2 deletions src/user-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from "./narou-search-results.js";
import type { UserFields, UserOrder, UserSearchParams } from "./params.js";
import { SearchBuilderBase } from "./search-builder.js";
import type { ExecuteOptions } from "./narou.js";

/**
* なろうユーザ検索API
Expand Down Expand Up @@ -102,9 +103,15 @@ export default class UserSearchBuilder<

/**
* なろう小説APIへのリクエストを実行する
* @param options 実行オプション
* @returns ランキング
*/
execute(): Promise<NarouSearchResults<UserSearchResult, TField>> {
return this.api.executeUserSearch(this.params as UserSearchParams);
execute(
options?: ExecuteOptions
): Promise<NarouSearchResults<UserSearchResult, TField>> {
return this.api.executeUserSearch(
this.params as UserSearchParams,
options
);
}
}
19 changes: 19 additions & 0 deletions test/narou-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,23 @@ describe('NarouNovelFetch', () => {
// リクエストが呼ばれたことを確認
expect(requestSpy).toHaveBeenCalled();
});

it('should pass fetchOptions to fetch', async () => {
// MSWでエンドポイントをモック
server.use(
http.get('https://api.example.com', ({ request }) => {
expect(request.headers.get('user-agent')).toBe('node-narou');
return responseGzipOrJson(mockData, new URL(request.url));
})
);

const narouFetch = new NarouNovelFetch();

// @ts-expect-error - Accessing protected method for testing
await narouFetch.execute(
{ gzip: 0 },
'https://api.example.com',
{ fetchOptions: { headers: { 'user-agent': 'node-narou' } } }
);
});
});
Loading
Loading