eve-esi-types 1.0.0 → 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 CHANGED
@@ -1,2 +1,12 @@
1
1
  # eve-esi-types
2
- Extracted the main type of ESI. use for ESI request response types
2
+ Extracted the main type of ESI. Used for ESI request response types.
3
+
4
+ ## Usage
5
+
6
+ > Sample code is provided -> [`esi-request.mjs`](./esi-request.mjs)
7
+
8
+
9
+ ```shell
10
+ $ node esi-request.mjs
11
+ ```
12
+
@@ -0,0 +1,248 @@
1
+ // import type { TESIResponseOKMap } from "eve-esi-types";
2
+ // - - - - - - - - - - - - - - - - - - - -
3
+ // constants, types
4
+ // - - - - - - - - - - - - - - - - - - - -
5
+ // shorthands
6
+ const log = console.log;
7
+ const isArray = Array.isArray;
8
+ /**
9
+ * enable/disable console.log
10
+ */
11
+ let LOG = false;
12
+ /**
13
+ * this always `https://esi.evetech.net`
14
+ */
15
+ const BASE = "https://esi.evetech.net";
16
+ /**
17
+ * @typedef {import("./src").TESIResponseOKMap} TESIResponseOKMap
18
+ */
19
+ /**
20
+ * @typedef ESIRequestOptions
21
+ * @prop {Record<string, any>} [queries] query params for ESI request.
22
+ * @prop {any} [body] will need it for `POST` request etc.
23
+ * @prop {boolean} [ignoreError] if want response data with ignore error then can be set to `true`.
24
+ * @prop {AbortController} [cancelable] cancel request immediately
25
+ * @prop {string} [token] Can be an empty object if no authentication is required.description
26
+ */
27
+ // - - - - - - - - - - - - - - - - - - - -
28
+ // module vars, functions
29
+ // - - - - - - - - - - - - - - - - - - - -
30
+ /**
31
+ * Get the number of currently executing ESI requests
32
+ */
33
+ let ax = 0;
34
+ /**
35
+ * simple named error class.
36
+ */
37
+ class ESIRequesError extends Error {
38
+ }
39
+ /**
40
+ * throws when x-esi-error-limit-remain header value is "0". (http status: 420)
41
+ */
42
+ class ESIErrorLimitReachedError extends Error {
43
+ constructor() {
44
+ super("Cannot continue ESI request because 'x-esi-error-limit-remain' is zero!");
45
+ }
46
+ valueOf() {
47
+ return 420;
48
+ }
49
+ }
50
+ /**
51
+ * fetch the extra pages
52
+ *
53
+ * + if the `x-pages` header property ware more than 1
54
+ *
55
+ * @param {string} endpointUrl
56
+ * @param {RequestInit} rqopt request options
57
+ * @param {Record<string, any>} qs queries
58
+ * @param {number} pc pageCount
59
+ */
60
+ const fetchP = async (endpointUrl, rqopt, qs, pc) => {
61
+ const rqs = [];
62
+ const rqp = new URLSearchParams(qs);
63
+ for (let i = 2; i <= pc;) {
64
+ rqp.set("page", (i++) + "");
65
+ ax++;
66
+ rqs.push(fetch(`${endpointUrl}?${rqp + ""}`, rqopt).then(res => res.json()).catch(reason => {
67
+ console.warn(reason);
68
+ return [];
69
+ }).finally(() => {
70
+ ax--;
71
+ }));
72
+ }
73
+ return Promise.all(rqs).then(jsons => {
74
+ // DEVNOTE: let check the page 2, type is array?
75
+ if (isArray(jsons[0])) {
76
+ let combined = [];
77
+ for (let i = 0, end = jsons.length; i < end;) {
78
+ combined = combined.concat(jsons[i++]);
79
+ }
80
+ return combined;
81
+ }
82
+ LOG && log("> > > pages result are object < < < --", jsons);
83
+ return null;
84
+ });
85
+ };
86
+ /** ### replace (C)urly (B)races (T)oken
87
+ *
88
+ * @example
89
+ * "/characters/{character_id}/skills"
90
+ * // ->
91
+ * "/characters/<char.character_id>/skills"
92
+ *
93
+ * @param {string} endpoint e.g - "/characters/{character_id}/"
94
+ * @param {number[]} ids
95
+ * @returns fragment of qualified endpoint uri or null.
96
+ */
97
+ const replaceCbt = (endpoint, ids) => {
98
+ const re = /{([\w]+)}/g;
99
+ /** @type {RegExpExecArray?} */
100
+ let m;
101
+ let idx = 0;
102
+ while (m = re.exec(endpoint)) {
103
+ endpoint = endpoint.replace(m[0], ids[idx++] + "");
104
+ }
105
+ return endpoint;
106
+ };
107
+ /**
108
+ *
109
+ * @param {string} endp this means endpoint url fragment like `/characters/{character_id}/` or `/characters/{character_id}/agents_research/`
110
+ * + The version parameter can be omitted by using `<version>/<endpoint>`
111
+ */
112
+ const curl = (endp) => {
113
+ endp = endp.replace(/^\/+|\/+$/g, "");
114
+ return `${BASE}/latest/${endp}/`;
115
+ };
116
+ // - - - - - - - - - - - - - - - - - - - -
117
+ // main functions
118
+ // - - - - - - - - - - - - - - - - - - - -
119
+ /**
120
+ * fire ESI request
121
+ * @template {"get" | "post" | "put" | "delete"} M
122
+ * @template {keyof TESIResponseOKMap[M]} EP
123
+ * @template {TESIResponseOKMap[M][EP]} R
124
+ *
125
+ * @param {M} mthd
126
+ * @param {EP} endp - The endpoint to request.
127
+ * @param {number | number[] | ESIRequestOptions} [pathParams] - Optional path parameters.
128
+ * @param {ESIRequestOptions} [opt] - default is empty object {}. `body` is json string
129
+ * @returns {Promise<R>} - The response from the endpoint.
130
+ * @throws
131
+ * @async
132
+ */
133
+ export async function fire(mthd, endp, pathParams, opt = {}) {
134
+ if (typeof pathParams === "number") {
135
+ pathParams = /** @type {number[]} */ ([pathParams]);
136
+ }
137
+ if (isArray(pathParams)) {
138
+ // @ts-ignore actualy endp is string
139
+ endp = replaceCbt(endp, pathParams);
140
+ }
141
+ else {
142
+ // When only options are provided
143
+ opt = /** @type {ESIRequestOptions} */ (pathParams) || opt;
144
+ }
145
+ /** @type {RequestInit} */
146
+ const rqopt = {
147
+ method: mthd,
148
+ mode: "cors",
149
+ cache: "no-cache",
150
+ signal: opt.cancelable?.signal,
151
+ headers: {}
152
+ };
153
+ const qss = {
154
+ // language: "en",
155
+ };
156
+ if (opt.queries) {
157
+ // Object.assign(queries, options.queries); Object.assign is too slow
158
+ const oqs = opt.queries;
159
+ for (const k of Object.keys(oqs)) {
160
+ qss[k] = oqs[k];
161
+ }
162
+ }
163
+ // DEVNOTE: when datasource is not empty string. (e.g - "singularity"
164
+ // in this case must specify datasource.
165
+ // disabled since `REMOVING DATASOURCE SINGULARITY`
166
+ // if (opt.datasource === "singularity") {
167
+ // opt.datasource = "tranquility";
168
+ // }
169
+ if (opt.token) {
170
+ // @ts-ignore The header is indeed an object
171
+ rqopt.headers.authorization = `Bearer ${opt.token}`;
172
+ }
173
+ if (opt.body) { // means "POST" method etc
174
+ // @ts-ignore The header is indeed an object
175
+ rqopt.headers["content-type"] = "application/json";
176
+ rqopt.body = JSON.stringify(opt.body);
177
+ }
178
+ // @ts-ignore actualy endp is string
179
+ const endpointUrl = curl(endp);
180
+ ax++;
181
+ try {
182
+ // @ts-ignore A silly type error will appear, but ignore it.
183
+ const res = await fetch(`${endpointUrl}?${new URLSearchParams(qss) + ""}`, rqopt).finally(() => {
184
+ ax--;
185
+ });
186
+ const stat = res.status;
187
+ if (!res.ok && !opt.ignoreError) {
188
+ if (stat === 420) {
189
+ opt.cancelable && opt.cancelable.abort();
190
+ throw new ESIErrorLimitReachedError();
191
+ }
192
+ else {
193
+ // @ts-ignore actualy endp is string
194
+ throw new ESIRequesError(`maybe network disconneted or otherwise request data are invalid. (endpoint=${endp}, http status=${stat})`);
195
+ }
196
+ }
197
+ else {
198
+ // DEVNOTE: - 204 No Content
199
+ if (stat === 204) {
200
+ // this result is empty, decided to return status code.
201
+ return /** @type {R} */ ({ status: stat });
202
+ }
203
+ /** @type {R} */
204
+ const data = await res.json();
205
+ if (opt.ignoreError) {
206
+ // meaning `forceJson`?
207
+ return data;
208
+ }
209
+ // - - - - x-pages response.
210
+ // +undefined is NaN
211
+ // @ts-ignore becouse +null is 0
212
+ const pc = +res.headers.get("x-pages");
213
+ // has remaining pages? NaN > 1 === false !isNaN(pageCount)
214
+ if (pc > 1) {
215
+ LOG && log('found "x-pages" header, pages: %d', pc);
216
+ const remData = await fetchP(endpointUrl, rqopt, qss, pc);
217
+ // finally, decide product data.
218
+ if (isArray(data) && isArray(remData)) {
219
+ // DEVNOTE: 2019/7/23 15:01:48 - types
220
+ return /** @type {R} */ (data.concat(remData));
221
+ }
222
+ else {
223
+ // @ts-ignore TODO: fix type
224
+ remData && Object.assign(data, remData);
225
+ }
226
+ }
227
+ return data;
228
+ }
229
+ }
230
+ catch (e) {
231
+ // @ts-ignore actualy endp is string
232
+ throw new ESIRequesError(`unknown error occurred, message: ${e.message}, endpoint=${endp}`);
233
+ }
234
+ }
235
+ // It should complete correctly.
236
+ async function getEVEStatus() {
237
+ return fire("get", "/status/");
238
+ }
239
+ // type following and run
240
+ // tsc
241
+ // then run
242
+ // node esi-request.mjs
243
+ getEVEStatus().then(eveStatus => console.log(eveStatus));
244
+ // {
245
+ // "players": 16503,
246
+ // "server_version": "2794925",
247
+ // "start_time": "2025-01-21T11:02:34Z"
248
+ // }
@@ -0,0 +1,309 @@
1
+ /*!
2
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
3
+ Copyright (C) 2025 jeffy-g <hirotom1107@gmail.com>
4
+ Released under the MIT license
5
+ https://opensource.org/licenses/mit-license.php
6
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
7
+ */
8
+ // - - - - - - - - - - - - - - - - - - - -
9
+ // imports
10
+ // - - - - - - - - - - - - - - - - - - - -
11
+ import type { TESIResponseOKMap } from "./src";
12
+ // import type { TESIResponseOKMap } from "eve-esi-types";
13
+
14
+
15
+ // - - - - - - - - - - - - - - - - - - - -
16
+ // constants, types
17
+ // - - - - - - - - - - - - - - - - - - - -
18
+ // shorthands
19
+ const log = console.log;
20
+ const isArray = Array.isArray;
21
+ /**
22
+ * enable/disable console.log
23
+ */
24
+ let LOG = false;
25
+ /**
26
+ * this always `https://esi.evetech.net`
27
+ */
28
+ const BASE = "https://esi.evetech.net";
29
+
30
+
31
+ type TRequestMethod = "get" | "post" | "put" | "delete";
32
+ type ESIRequestOptions = {
33
+ /**
34
+ * query params for ESI request.
35
+ */
36
+ queries?: Record<string, any>;
37
+ /**
38
+ * will need it for `POST` request etc.
39
+ */
40
+ body?: any;
41
+ /**
42
+ * if want response data with ignore error then can be set to `true`.
43
+ */
44
+ ignoreError?: boolean;
45
+ /**
46
+ * cancel request immediately
47
+ */
48
+ cancelable?: AbortController;
49
+
50
+ /**
51
+ * Can be an empty object if no authentication is required.description
52
+ */
53
+ token?: string;
54
+ };
55
+ /**
56
+ * @typedef {import("./src").TESIResponseOKMap} TESIResponseOKMap
57
+ */
58
+ /**
59
+ * @typedef ESIRequestOptions
60
+ * @prop {Record<string, any>} [queries] query params for ESI request.
61
+ * @prop {any} [body] will need it for `POST` request etc.
62
+ * @prop {boolean} [ignoreError] if want response data with ignore error then can be set to `true`.
63
+ * @prop {AbortController} [cancelable] cancel request immediately
64
+ * @prop {string} [token] Can be an empty object if no authentication is required.description
65
+ */
66
+
67
+
68
+ // - - - - - - - - - - - - - - - - - - - -
69
+ // module vars, functions
70
+ // - - - - - - - - - - - - - - - - - - - -
71
+ /**
72
+ * Get the number of currently executing ESI requests
73
+ */
74
+ let ax: number = 0;
75
+ /**
76
+ * simple named error class.
77
+ */
78
+ class ESIRequesError extends Error {}
79
+ /**
80
+ * throws when x-esi-error-limit-remain header value is "0". (http status: 420)
81
+ */
82
+ class ESIErrorLimitReachedError extends Error {
83
+ constructor() {
84
+ super("Cannot continue ESI request because 'x-esi-error-limit-remain' is zero!");
85
+ }
86
+ valueOf(): number {
87
+ return 420;
88
+ }
89
+ }
90
+ /**
91
+ * fetch the extra pages
92
+ *
93
+ * + if the `x-pages` header property ware more than 1
94
+ *
95
+ * @param {string} endpointUrl
96
+ * @param {RequestInit} rqopt request options
97
+ * @param {Record<string, any>} qs queries
98
+ * @param {number} pc pageCount
99
+ */
100
+ const fetchP = async <T extends any>(endpointUrl: string, rqopt: RequestInit, qs: Record<string, any>, pc: number) => {
101
+ const rqs: Promise<T>[] = [];
102
+ const rqp = new URLSearchParams(qs);
103
+ for (let i = 2; i <= pc; ) {
104
+ rqp.set("page", (i++) + "");
105
+ ax++;
106
+ rqs.push(
107
+ fetch(`${endpointUrl}?${rqp + ""}`, rqopt).then(
108
+ res => res.json()
109
+ ).catch(reason => {
110
+ console.warn(reason);
111
+ return [] as T;
112
+ }).finally(() => {
113
+ ax--;
114
+ })
115
+ );
116
+ }
117
+ return Promise.all(rqs).then(jsons => {
118
+ // DEVNOTE: let check the page 2, type is array?
119
+ if (isArray(jsons[0])) {
120
+ let combined: any[] = [];
121
+ for (let i = 0, end = jsons.length; i < end;) {
122
+ combined = combined.concat(jsons[i++]);
123
+ }
124
+ return combined as T;
125
+ }
126
+
127
+ LOG && log("> > > pages result are object < < < --", jsons);
128
+ return null;
129
+ });
130
+ };
131
+
132
+ /** ### replace (C)urly (B)races (T)oken
133
+ *
134
+ * @example
135
+ * "/characters/{character_id}/skills"
136
+ * // ->
137
+ * "/characters/<char.character_id>/skills"
138
+ *
139
+ * @param {string} endpoint e.g - "/characters/{character_id}/"
140
+ * @param {number[]} ids
141
+ * @returns fragment of qualified endpoint uri or null.
142
+ */
143
+ const replaceCbt = (endpoint: string, ids: number[]) => {
144
+ const re = /{([\w]+)}/g;
145
+ /** @type {RegExpExecArray?} */
146
+ let m: RegExpExecArray | null;
147
+ let idx = 0
148
+ while (m = re.exec(endpoint)) {
149
+ endpoint = endpoint.replace(m[0], ids[idx++] + "");
150
+ }
151
+ return endpoint;
152
+ };
153
+ /**
154
+ *
155
+ * @param {string} endp this means endpoint url fragment like `/characters/{character_id}/` or `/characters/{character_id}/agents_research/`
156
+ * + The version parameter can be omitted by using `<version>/<endpoint>`
157
+ */
158
+ const curl = (endp: string) => {
159
+ endp = endp.replace(/^\/+|\/+$/g, "");
160
+ return `${BASE}/latest/${endp}/`;
161
+ };
162
+
163
+
164
+ // - - - - - - - - - - - - - - - - - - - -
165
+ // main functions
166
+ // - - - - - - - - - - - - - - - - - - - -
167
+ /**
168
+ * fire ESI request
169
+ * @template {"get" | "post" | "put" | "delete"} M
170
+ * @template {keyof TESIResponseOKMap[M]} EP
171
+ * @template {TESIResponseOKMap[M][EP]} R
172
+ *
173
+ * @param {M} mthd
174
+ * @param {EP} endp - The endpoint to request.
175
+ * @param {number | number[] | ESIRequestOptions} [pathParams] - Optional path parameters.
176
+ * @param {ESIRequestOptions} [opt] - default is empty object {}. `body` is json string
177
+ * @returns {Promise<R>} - The response from the endpoint.
178
+ * @throws
179
+ * @async
180
+ */
181
+ export async function fire<
182
+ M extends TRequestMethod,
183
+ EP extends keyof TESIResponseOKMap[M],
184
+ R extends TESIResponseOKMap[M][EP]
185
+ >(
186
+ mthd: M, endp: EP, pathParams?: number | number[] | ESIRequestOptions,
187
+ opt: ESIRequestOptions = {}
188
+ ): Promise<R> {
189
+
190
+ if (typeof pathParams === "number") {
191
+ pathParams = /** @type {number[]} */([pathParams]);
192
+ }
193
+ if (isArray(pathParams)) {
194
+ // @ts-ignore actualy endp is string
195
+ endp = replaceCbt(endp, pathParams) as EP;
196
+ } else {
197
+ // When only options are provided
198
+ opt = /** @type {ESIRequestOptions} */(pathParams) as ESIRequestOptions || opt;
199
+ }
200
+
201
+ /** @type {RequestInit} */
202
+ const rqopt: RequestInit = {
203
+ method: mthd,
204
+ mode: "cors",
205
+ cache: "no-cache",
206
+ signal: opt.cancelable?.signal,
207
+ headers: {}
208
+ };
209
+ const qss: Record<string, string> = {
210
+ // language: "en",
211
+ };
212
+
213
+ if (opt.queries) {
214
+ // Object.assign(queries, options.queries); Object.assign is too slow
215
+ const oqs = opt.queries;
216
+ for (const k of Object.keys(oqs)) {
217
+ qss[k] = oqs[k] as string;
218
+ }
219
+ }
220
+ // DEVNOTE: when datasource is not empty string. (e.g - "singularity"
221
+ // in this case must specify datasource.
222
+ // disabled since `REMOVING DATASOURCE SINGULARITY`
223
+ // if (opt.datasource === "singularity") {
224
+ // opt.datasource = "tranquility";
225
+ // }
226
+ if (opt.token) {
227
+ // @ts-ignore The header is indeed an object
228
+ (rqopt.headers as any).authorization = `Bearer ${opt.token}`;
229
+ }
230
+ if (opt.body) { // means "POST" method etc
231
+ // @ts-ignore The header is indeed an object
232
+ (rqopt.headers as any)["content-type"] = "application/json";
233
+ rqopt.body = JSON.stringify(opt.body);
234
+ }
235
+
236
+ // @ts-ignore actualy endp is string
237
+ const endpointUrl = curl(endp);
238
+ ax++;
239
+ try {
240
+ // @ts-ignore A silly type error will appear, but ignore it.
241
+ const res = await fetch(
242
+ `${endpointUrl}?${new URLSearchParams(qss) + ""}`, rqopt
243
+ ).finally(() => {
244
+ ax--;
245
+ });
246
+
247
+ const stat = res.status;
248
+ if (!res.ok && !opt.ignoreError) {
249
+ if (stat === 420) {
250
+ opt.cancelable && opt.cancelable.abort();
251
+ throw new ESIErrorLimitReachedError();
252
+ } else {
253
+ // @ts-ignore actualy endp is string
254
+ throw new ESIRequesError(`maybe network disconneted or otherwise request data are invalid. (endpoint=${endp}, http status=${stat})`);
255
+ }
256
+ } else {
257
+ // DEVNOTE: - 204 No Content
258
+ if (stat === 204) {
259
+ // this result is empty, decided to return status code.
260
+ return /** @type {R} */({ status: stat }) as unknown as R;
261
+ }
262
+
263
+ /** @type {R} */
264
+ const data: R = await res.json();
265
+ if (opt.ignoreError) {
266
+ // meaning `forceJson`?
267
+ return data;
268
+ }
269
+ // - - - - x-pages response.
270
+ // +undefined is NaN
271
+ // @ts-ignore becouse +null is 0
272
+ const pc = +res.headers.get("x-pages")!;
273
+ // has remaining pages? NaN > 1 === false !isNaN(pageCount)
274
+ if (pc > 1) {
275
+ LOG && log('found "x-pages" header, pages: %d', pc);
276
+ const remData = await fetchP<R>(endpointUrl, rqopt, qss, pc);
277
+ // finally, decide product data.
278
+ if (isArray(data) && isArray(remData)) {
279
+ // DEVNOTE: 2019/7/23 15:01:48 - types
280
+ return /** @type {R} */(data.concat(remData)) as unknown as R;
281
+ } else {
282
+ // @ts-ignore TODO: fix type
283
+ remData && Object.assign(data, remData);
284
+ }
285
+ }
286
+
287
+ return data;
288
+ }
289
+
290
+ } catch (e) {
291
+ // @ts-ignore actualy endp is string
292
+ throw new ESIRequesError(`unknown error occurred, message: ${(e as Error).message}, endpoint=${endp}`);
293
+ }
294
+ }
295
+
296
+ // It should complete correctly.
297
+ async function getEVEStatus() {
298
+ (await fire("get", "/characters/{character_id}/ship/", 994562, { token: "<accessToken of 994562>" })).result;
299
+ return fire("get", "/status/");
300
+ }
301
+
302
+
303
+ // node esi-request.mjs
304
+ getEVEStatus().then(eveStatus => console.log(eveStatus));
305
+ // {
306
+ // "players": 16503,
307
+ // "server_version": "2794925",
308
+ // "start_time": "2025-01-21T11:02:34Z"
309
+ // }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-esi-types",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Extracted the main type of ESI. use for ESI request response types",
5
5
  "main": "src/index.d.ts",
6
6
  "scripts": {
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "skipDefaultLibCheck": true,
5
+ "skipLibCheck": true,
6
+ "outDir": ".",
7
+ "target": "esnext",
8
+ "module": "esnext",
9
+ // "declaration": true,
10
+ "diagnostics": true,
11
+ "newLine": "LF",
12
+ "strict": true,
13
+ "moduleResolution": "node",
14
+ "rootDirs": [
15
+ "./"
16
+ ]
17
+ },
18
+ "include": [
19
+ "./esi-request.mts"
20
+ ],
21
+ "exclude": []
22
+ }