@funmary/client 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/dist/LICENSE-APACHE-2.0 +201 -0
- package/dist/LICENSE-BSD-3-CLAUSE +28 -0
- package/dist/index.d.ts +3251 -0
- package/dist/index.js +139 -0
- package/package.json +36 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createFetch, ofetch } from "ofetch";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
/** サーバーが誤りを返したときと、通信できなかったときの例外。トークンは含めない */
|
|
4
|
+
var FunmaryApiError = class extends Error {
|
|
5
|
+
/** HTTP の状態。通信できなかったときは 0 */
|
|
6
|
+
status;
|
|
7
|
+
/** サーバーが返した誤りの種類 (not_found、invalid_request、terms_not_accepted など)。通信の失敗は network_error */
|
|
8
|
+
code;
|
|
9
|
+
/** サーバーが返した本文。JSON でなければ undefined */
|
|
10
|
+
body;
|
|
11
|
+
constructor(init) {
|
|
12
|
+
super(init.message);
|
|
13
|
+
this.name = "FunmaryApiError";
|
|
14
|
+
this.status = init.status;
|
|
15
|
+
this.code = init.code;
|
|
16
|
+
this.body = init.body;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
20
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
21
|
+
const DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
22
|
+
/** Retry-After が長すぎても、これ以上は待たない */
|
|
23
|
+
const MAX_RETRY_DELAY_MS = 6e4;
|
|
24
|
+
/** Retry-After (秒、または日時) を、待つ時間 (ミリ秒) にする */
|
|
25
|
+
function retryDelayMs(header, attempt, now) {
|
|
26
|
+
if (header !== null && /^\d+$/.test(header.trim())) return Math.min(Number(header) * 1e3, MAX_RETRY_DELAY_MS);
|
|
27
|
+
const date = header === null ? NaN : Date.parse(header);
|
|
28
|
+
if (!Number.isNaN(date)) return Math.min(Math.max(date - now, 0), MAX_RETRY_DELAY_MS);
|
|
29
|
+
return DEFAULT_RETRY_DELAY_MS * (attempt + 1);
|
|
30
|
+
}
|
|
31
|
+
const segment = (value) => encodeURIComponent(String(value));
|
|
32
|
+
function createFunmaryClient(options) {
|
|
33
|
+
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
34
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
35
|
+
const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
36
|
+
const http = (options.fetch ? createFetch({ fetch: options.fetch }) : ofetch).create({
|
|
37
|
+
baseURL: options.baseURL,
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${options.token}`,
|
|
40
|
+
Accept: "application/json"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
async function request(method, path, extra = {}) {
|
|
44
|
+
for (let attempt = 0;; attempt += 1) {
|
|
45
|
+
let response;
|
|
46
|
+
try {
|
|
47
|
+
response = await http.raw(path, {
|
|
48
|
+
method,
|
|
49
|
+
...extra.query ? { query: extra.query } : {},
|
|
50
|
+
...extra.body === void 0 ? {} : { body: extra.body },
|
|
51
|
+
timeout,
|
|
52
|
+
retry: false,
|
|
53
|
+
ignoreResponseError: true
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new FunmaryApiError({
|
|
57
|
+
status: 0,
|
|
58
|
+
code: "network_error",
|
|
59
|
+
message: `Funmary に接続できませんでした: ${error instanceof Error ? error.message : String(error)}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 429 && attempt < maxRetries) {
|
|
63
|
+
await sleep(retryDelayMs(response.headers.get("retry-after"), attempt, Date.now()));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const body = response._data;
|
|
68
|
+
const record = typeof body === "object" && body !== null ? body : {};
|
|
69
|
+
throw new FunmaryApiError({
|
|
70
|
+
status: response.status,
|
|
71
|
+
code: typeof record["error"] === "string" ? record["error"] : `http_${response.status}`,
|
|
72
|
+
message: typeof record["message"] === "string" ? record["message"] : `Funmary が HTTP ${response.status} を返しました`,
|
|
73
|
+
body: typeof body === "object" ? body : void 0
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return response._data;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const range = (r) => ({
|
|
80
|
+
start: r.start,
|
|
81
|
+
end: r.end
|
|
82
|
+
});
|
|
83
|
+
const course = (ref) => `/api/v1/courses/${segment(ref.year)}/${segment(ref.syllabusId)}`;
|
|
84
|
+
return {
|
|
85
|
+
/** トークンの持ち主と、許された範囲 */
|
|
86
|
+
me: () => request("GET", "/api/v1/me"),
|
|
87
|
+
/** 休講などのデータが新しいか */
|
|
88
|
+
status: () => request("GET", "/api/v1/status"),
|
|
89
|
+
lessons: (r) => request("GET", "/api/v1/lessons", { query: range(r) }),
|
|
90
|
+
nextLesson: () => request("GET", "/api/v1/lessons/next"),
|
|
91
|
+
/** 授業と、振替授業日、全学の休講日、祝日 */
|
|
92
|
+
timetable: (r) => request("GET", "/api/v1/timetable", { query: range(r) }),
|
|
93
|
+
/** 週の曜日と時限の格子。date を省くと今週 */
|
|
94
|
+
week: (query = {}) => request("GET", "/api/v1/timetable/week", { query }),
|
|
95
|
+
periods: () => request("GET", "/api/v1/periods"),
|
|
96
|
+
academicCalendar: (query = {}) => request("GET", "/api/v1/academic-calendar", { query }),
|
|
97
|
+
/** 休講、補講、教室変更 */
|
|
98
|
+
changes: (r) => request("GET", "/api/v1/changes", { query: range(r) }),
|
|
99
|
+
courses: {
|
|
100
|
+
list: () => request("GET", "/api/v1/courses"),
|
|
101
|
+
/** 履修登録する。登録済みなら、何も変えずに今の状態を返す */
|
|
102
|
+
register: (ref) => request("POST", "/api/v1/courses", { body: ref }),
|
|
103
|
+
/** 履修登録を取り消す。自分だけの曜日と時限と略称は残る */
|
|
104
|
+
unregister: (ref) => request("DELETE", course(ref)),
|
|
105
|
+
/** 自分だけに使う曜日と時限を足す (同じ曜日と時限があれば、教室を上書きする) */
|
|
106
|
+
setPersonalSlot: (ref, slot) => request("PUT", `${course(ref)}/personal-slots`, { body: slot }),
|
|
107
|
+
removePersonalSlot: (ref, slot) => request("DELETE", `${course(ref)}/personal-slots/${segment(slot.weekday)}/${segment(slot.period)}`),
|
|
108
|
+
/** 時間割に出す、自分だけの略称。空にすると、正式名称を使う */
|
|
109
|
+
setAbbreviation: (ref, abbreviation) => request("PUT", `${course(ref)}/abbreviation`, { body: { abbreviation } })
|
|
110
|
+
},
|
|
111
|
+
subjects: {
|
|
112
|
+
search: (query = {}) => request("GET", "/api/v1/subjects", { query }),
|
|
113
|
+
get: (ref) => request("GET", `/api/v1/subjects/${segment(ref.year)}/${segment(ref.syllabusId)}`),
|
|
114
|
+
sessions: (ref) => request("GET", `/api/v1/subjects/${segment(ref.year)}/${segment(ref.syllabusId)}/sessions`)
|
|
115
|
+
},
|
|
116
|
+
events: {
|
|
117
|
+
list: (r) => request("GET", "/api/v1/events", { query: range(r) }),
|
|
118
|
+
get: (id) => request("GET", `/api/v1/events/${segment(id)}`),
|
|
119
|
+
create: (body) => request("POST", "/api/v1/events", { body }),
|
|
120
|
+
/** 送った内容に置き換える (送らなかった項目は、空になる) */
|
|
121
|
+
replace: (id, body) => request("PUT", `/api/v1/events/${segment(id)}`, { body }),
|
|
122
|
+
delete: (id) => request("DELETE", `/api/v1/events/${segment(id)}`)
|
|
123
|
+
},
|
|
124
|
+
notifications: {
|
|
125
|
+
list: (query = {}) => request("GET", "/api/v1/notifications", { query }),
|
|
126
|
+
markRead: (id) => request("POST", `/api/v1/notifications/${segment(id)}/read`),
|
|
127
|
+
markAllRead: () => request("POST", "/api/v1/notifications/read")
|
|
128
|
+
},
|
|
129
|
+
bus: {
|
|
130
|
+
stops: () => request("GET", "/api/v1/bus/stops"),
|
|
131
|
+
stop: (stopId, query = {}) => request("GET", `/api/v1/bus/stops/${segment(stopId)}`, { query }),
|
|
132
|
+
next: (query = {}) => request("GET", "/api/v1/bus/next", { query }),
|
|
133
|
+
/** 「A から未来大へ」「未来大から A へ」の発車。A は、未来大を通る任意のバス停 */
|
|
134
|
+
legs: (query) => request("GET", "/api/v1/bus/legs", { query })
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { FunmaryApiError, createFunmaryClient };
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funmary/client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Funmary の公開 API の型付きクライアント",
|
|
5
|
+
"license": "(BSD-3-Clause OR Apache-2.0)",
|
|
6
|
+
"author": "funmary (https://github.com/funmary-app)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/funmary-app/funmary.git",
|
|
10
|
+
"directory": "packages/public/client"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"ofetch": "^1.5.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsdown": "^0.23.0"
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "34673f7181da7961d29934dd0bfb18f091c45ba7",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown"
|
|
38
|
+
}
|
|
6
39
|
}
|