Skip to content

Commit 7468f53

Browse files
authored
Merge pull request #1360 from neet/introduce-raw-2
feat: Introduce experimental `$raw` method that allows access to headers
2 parents 1d4d313 + b5736b1 commit 7468f53

9 files changed

Lines changed: 153 additions & 50 deletions

File tree

examples/raw-method.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createRestAPIClient } from "masto";
2+
3+
const masto = createRestAPIClient({
4+
url: "https://example.com",
5+
accessToken: "TOKEN",
6+
});
7+
8+
const { data: me, headers } = await masto.v1.accounts.verifyCredentials.$raw();
9+
console.log(`logged in as ${me.acct}`);
10+
console.log(headers.get("X-RateLimit-Remaining"));
11+
12+
for await (const { data, headers } of masto.v1.timelines.public.list.$raw({
13+
local: true,
14+
})) {
15+
console.log(`data.length=${data.length}`);
16+
console.log(headers.get("X-RateLimit-Remaining"));
17+
}

src/adapters/action/dispatcher-http-hook-mastodon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class HttpActionDispatcherHookMastodon
100100
const encoding = inferEncoding(type, path);
101101
const meta: HttpMetaParams<Encoding> = { ...action.meta, encoding };
102102

103-
return { type, path, data: action.data, meta };
103+
return { ...action, type, path, meta };
104104
}
105105

106106
dispatch(action: AnyAction): false | Promise<unknown> {

src/adapters/action/dispatcher-http.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,34 @@ export class HttpActionDispatcher implements ActionDispatcher<HttpAction> {
2929
case "fetch": {
3030
result = this.http
3131
.get(action.path, action.data, action.meta)
32-
.then((r) => r.data as T);
32+
.then((r) => (action.raw ? r : r.data) as T);
3333
break;
3434
}
3535
case "create": {
3636
result = this.http
3737
.post(action.path, action.data, action.meta)
38-
.then((r) => r.data as T);
38+
.then((r) => (action.raw ? r : r.data) as T);
3939
break;
4040
}
4141
case "update": {
4242
result = this.http
4343
.put(action.path, action.data, action.meta)
44-
.then((r) => r.data as T);
44+
.then((r) => (action.raw ? r : r.data) as T);
4545
break;
4646
}
4747
case "remove": {
4848
result = this.http
4949
.delete(action.path, action.data, action.meta)
50-
.then((r) => r.data as T);
50+
.then((r) => (action.raw ? r : r.data) as T);
5151
break;
5252
}
5353
case "list": {
54-
result = new PaginatorHttp(this.http, action.path, action.data) as T;
54+
result = new PaginatorHttp(
55+
this.http,
56+
action.raw,
57+
action.path,
58+
action.data,
59+
) as T;
5560
break;
5661
}
5762
}

src/adapters/action/paginator-http.spec.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import { HttpMockImpl } from "../../__mocks__/index.js";
2-
import { type HttpRequestParams } from "../../interfaces/index.js";
2+
import {
3+
type HttpRequestParams,
4+
type HttpResponse,
5+
} from "../../interfaces/index.js";
36
import { PaginatorHttp } from "./paginator-http.js";
47

58
describe("PaginatorHttp", () => {
69
const http = new HttpMockImpl();
710

811
beforeEach(() => {
9-
http.request.mockReturnValue({ headers: new Headers({}) });
12+
http.request.mockResolvedValue({
13+
data: {},
14+
headers: new Headers(),
15+
});
1016
});
1117

1218
afterEach(() => {
1319
http.clear();
1420
});
1521

1622
it("sends a request", async () => {
17-
const paginator = new PaginatorHttp(http, "/v1/api/timelines", {
23+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines", {
1824
foo: "bar",
1925
}).values();
2026
await paginator.next();
@@ -26,7 +32,7 @@ describe("PaginatorHttp", () => {
2632
});
2733

2834
it("sends a request with await", async () => {
29-
const paginator = new PaginatorHttp(http, "/v1/api/timelines", {
35+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines", {
3036
foo: "bar",
3137
});
3238
await paginator;
@@ -38,12 +44,16 @@ describe("PaginatorHttp", () => {
3844
});
3945

4046
it("parses the next url", async () => {
41-
http.request.mockReturnValue({
47+
http.request.mockResolvedValue({
4248
headers: new Headers({
4349
link: '<https://mastodon.social/api/v1/timelines/home?max_id=109382006402042919>; rel="next", <https://mastodon.social/api/v1/timelines/home?min_id=109382039876197520>; rel="prev"',
4450
}),
4551
});
46-
const paginator = new PaginatorHttp(http, "/v1/api/timelines").values();
52+
const paginator = new PaginatorHttp(
53+
http,
54+
false,
55+
"/v1/api/timelines",
56+
).values();
4757
await paginator.next();
4858
await paginator.next();
4959
expect(http.request).toBeCalledWith({
@@ -54,14 +64,14 @@ describe("PaginatorHttp", () => {
5464
});
5565

5666
it("paginates with opposite direction when prev direction was set", async () => {
57-
http.request.mockReturnValue({
67+
http.request.mockResolvedValue({
5868
headers: new Headers({
5969
link: '<https://mastodon.social/api/v1/timelines/home?max_id=109382006402042919>; rel="next", <https://mastodon.social/api/v1/timelines/home?min_id=109382039876197520>; rel="prev"',
6070
}),
6171
data: [],
6272
});
6373

64-
let paginator = new PaginatorHttp(http, "/v1/api/timelines");
74+
let paginator = new PaginatorHttp(http, false, "/v1/api/timelines");
6575
expect(paginator.getDirection()).toBe("next");
6676

6777
paginator = paginator.setDirection("prev");
@@ -78,12 +88,16 @@ describe("PaginatorHttp", () => {
7888
});
7989

8090
it("parses the next url regardless of order", async () => {
81-
http.request.mockReturnValue({
91+
http.request.mockResolvedValue({
8292
headers: new Headers({
8393
link: '<https://mastodon.social/api/v1/timelines/home?min_id=109382039876197520>; rel="prev", <https://mastodon.social/api/v1/timelines/home?max_id=109382006402042919>; rel="next"',
8494
}),
8595
});
86-
const paginator = new PaginatorHttp(http, "/v1/api/timelines").values();
96+
const paginator = new PaginatorHttp(
97+
http,
98+
false,
99+
"/v1/api/timelines",
100+
).values();
87101
await paginator.next();
88102
await paginator.next();
89103
expect(http.request).toBeCalledWith({
@@ -94,7 +108,11 @@ describe("PaginatorHttp", () => {
94108
});
95109

96110
it("returns done when next link does not exist", async () => {
97-
const paginator = new PaginatorHttp(http, "/v1/api/timelines").values();
111+
const paginator = new PaginatorHttp(
112+
http,
113+
false,
114+
"/v1/api/timelines",
115+
).values();
98116
await paginator.next();
99117
const result = await paginator.next();
100118
expect(result).toEqual({
@@ -103,7 +121,7 @@ describe("PaginatorHttp", () => {
103121
});
104122

105123
it("is AsyncIterable", async () => {
106-
const paginator = new PaginatorHttp(http, "/v1/api/timelines", {
124+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines", {
107125
foo: "bar",
108126
});
109127

@@ -119,7 +137,7 @@ describe("PaginatorHttp", () => {
119137
});
120138

121139
it("returns AsyncIterable from values", async () => {
122-
const paginator = new PaginatorHttp(http, "/v1/api/timelines", {
140+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines", {
123141
foo: "bar",
124142
});
125143

@@ -137,12 +155,12 @@ describe("PaginatorHttp", () => {
137155
});
138156

139157
it("parse array in url query string correctly", async () => {
140-
http.request.mockReturnValue({
158+
http.request.mockResolvedValue({
141159
headers: new Headers({
142160
link: '<https://mastodon.social/api/v1/notifications?types[]=mention&max_id=123456>; rel="next", <https://mastodon.social/api/v1/notifications?types[]=mention>; rel="prev"',
143161
}),
144162
});
145-
const paginator = new PaginatorHttp(http, "/v1/api/notifications", {
163+
const paginator = new PaginatorHttp(http, false, "/v1/api/notifications", {
146164
types: ["mention"],
147165
}).values();
148166
await paginator.next();
@@ -155,19 +173,19 @@ describe("PaginatorHttp", () => {
155173
});
156174

157175
it("is thenable", () => {
158-
const paginator = new PaginatorHttp(http, "/v1/api/timelines");
176+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines");
159177
const onFulfilled = vi.fn();
160178
paginator.then(onFulfilled);
161179
expect(onFulfilled).toBeCalledTimes(0);
162180
});
163181

164182
it("can be converted to promise", async () => {
165-
const paginator = new PaginatorHttp(http, "/v1/api/timelines");
183+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines");
166184
expect(paginator.then()).toBeInstanceOf(Promise);
167185
});
168186

169187
it("is thenable (error)", () => {
170-
const paginator = new PaginatorHttp(http, "/v1/api/timelines");
188+
const paginator = new PaginatorHttp(http, false, "/v1/api/timelines");
171189
http.request.mockImplementation(() => {
172190
throw new Error("mock error");
173191
});
@@ -178,4 +196,14 @@ describe("PaginatorHttp", () => {
178196
expect(onFulfilled).not.toBeCalled();
179197
expect(onRejected).toBeCalledTimes(0);
180198
});
199+
200+
it("respects the raw parameter", async () => {
201+
const paginator = new PaginatorHttp<HttpResponse<unknown>>(
202+
http,
203+
true,
204+
"/v1/api/timelines",
205+
);
206+
const result = await paginator;
207+
expect(result.headers).toBeInstanceOf(Headers);
208+
});
181209
});

src/adapters/action/paginator-http.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import { type Http, type HttpMetaParams } from "../../interfaces/index.js";
33
import { type mastodon } from "../../mastodon/index.js";
44
import { parseLinkHeader } from "../../utils/index.js";
55

6-
export class PaginatorHttp<Entity, Params = undefined>
7-
implements mastodon.Paginator<Entity, Params>
6+
export class PaginatorHttp<TEntity, TParams = undefined>
7+
implements mastodon.Paginator<TEntity, TParams>
88
{
99
constructor(
1010
private readonly http: Http,
11+
private readonly raw: boolean,
1112
private path?: string,
12-
private params?: Params | string,
13+
private params?: TParams | string,
1314
private readonly meta?: HttpMetaParams,
1415
private readonly direction: mastodon.Direction = "next",
1516
) {}
1617

17-
async *values(): AsyncIterableIterator<Entity> {
18+
async *values(): AsyncIterableIterator<TEntity> {
1819
let path = this.path;
1920
let params = this.params;
2021

@@ -30,15 +31,15 @@ export class PaginatorHttp<Entity, Params = undefined>
3031
path = nextUrl?.pathname;
3132
params = nextUrl?.search.replace(/^\?/, "");
3233

33-
const data = (await response.data) as Entity;
34+
const data = (this.raw ? response : response.data) as TEntity;
3435

3536
yield data;
3637
}
3738
}
3839

39-
then<TResult1 = Entity, TResult2 = never>(
40+
then<TResult1 = TEntity, TResult2 = never>(
4041
onfulfilled: (
41-
value: Entity,
42+
value: TEntity,
4243
) => TResult1 | PromiseLike<TResult1> = Promise.resolve.bind(Promise),
4344
onrejected: (
4445
reason: unknown,
@@ -53,9 +54,10 @@ export class PaginatorHttp<Entity, Params = undefined>
5354
return this.direction;
5455
}
5556

56-
setDirection(direction: mastodon.Direction): PaginatorHttp<Entity, Params> {
57+
setDirection(direction: mastodon.Direction): PaginatorHttp<TEntity, TParams> {
5758
return new PaginatorHttp(
5859
this.http,
60+
this.raw,
5961
this.path,
6062
this.params,
6163
this.meta,
@@ -64,9 +66,9 @@ export class PaginatorHttp<Entity, Params = undefined>
6466
}
6567

6668
[Symbol.asyncIterator](): AsyncIterator<
67-
Entity,
69+
TEntity,
6870
undefined,
69-
Params | string | undefined
71+
TParams | string | undefined
7072
> {
7173
return this.values();
7274
}

src/adapters/action/proxy.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,24 @@ describe("RequestBuilder", () => {
116116

117117
expect(dispatcher[Symbol.dispose]).toHaveBeenCalled();
118118
});
119+
120+
it("builds fetch action with raw option", () => {
121+
let action: AnyAction | undefined;
122+
123+
const builder: any = createActionProxy(
124+
{
125+
dispatch: async <T>(a: AnyAction) => {
126+
action = a;
127+
return {} as T;
128+
},
129+
},
130+
{ context: ["root"] },
131+
);
132+
const data = {};
133+
builder.$select("foo").bar.fetch.$raw(data);
134+
135+
expect(action?.type).toBe("fetch");
136+
expect(action?.path).toBe("/root/foo/bar");
137+
expect(action?.data).toBe(data);
138+
});
119139
});

src/adapters/action/proxy.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,8 @@ const get =
8181
const apply =
8282
<T>(actionDispatcher: ActionDispatcher<AnyAction>, context: string[]) =>
8383
(_1: unknown, _2: unknown, args: unknown[]): unknown => {
84-
const action = context.pop();
85-
86-
/* c8 ignore next 3 */
87-
if (!action) {
88-
throw new Error("No action specified");
89-
}
84+
let action = context.pop();
85+
let raw = false;
9086

9187
if (action === "$select") {
9288
return createActionProxy<T>(actionDispatcher, {
@@ -95,6 +91,16 @@ const apply =
9591
});
9692
}
9793

94+
if (action === "$raw") {
95+
action = context.pop();
96+
raw = true;
97+
}
98+
99+
/* c8 ignore next 3 */
100+
if (!action) {
101+
throw new Error("No action specified");
102+
}
103+
98104
const path = "/" + context.join("/");
99105
const [data, meta] = args;
100106

@@ -103,5 +109,6 @@ const apply =
103109
path,
104110
data,
105111
meta: meta as HttpMetaParams,
112+
raw,
106113
});
107114
};

src/interfaces/action.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Action<T extends string> {
66
readonly path: string;
77
readonly data: unknown;
88
readonly meta: HttpMetaParams<Encoding>;
9+
readonly raw: boolean;
910
}
1011

1112
export type AnyAction = Action<string>;

0 commit comments

Comments
 (0)