@pierre/storage 0.9.2 → 1.0.0

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/src/fetch.ts CHANGED
@@ -3,151 +3,156 @@ import type { ValidAPIVersion, ValidMethod, ValidPath } from './types';
3
3
  import { getUserAgent } from './version';
4
4
 
5
5
  interface RequestOptions {
6
- allowedStatus?: number[];
6
+ allowedStatus?: number[];
7
7
  }
8
8
 
9
9
  export class ApiError extends Error {
10
- public readonly status: number;
11
- public readonly statusText: string;
12
- public readonly method: ValidMethod;
13
- public readonly url: string;
14
- public readonly body?: unknown;
15
-
16
- constructor(params: {
17
- message: string;
18
- status: number;
19
- statusText: string;
20
- method: ValidMethod;
21
- url: string;
22
- body?: unknown;
23
- }) {
24
- super(params.message);
25
- this.name = 'ApiError';
26
- this.status = params.status;
27
- this.statusText = params.statusText;
28
- this.method = params.method;
29
- this.url = params.url;
30
- this.body = params.body;
31
- }
10
+ public readonly status: number;
11
+ public readonly statusText: string;
12
+ public readonly method: ValidMethod;
13
+ public readonly url: string;
14
+ public readonly body?: unknown;
15
+
16
+ constructor(params: {
17
+ message: string;
18
+ status: number;
19
+ statusText: string;
20
+ method: ValidMethod;
21
+ url: string;
22
+ body?: unknown;
23
+ }) {
24
+ super(params.message);
25
+ this.name = 'ApiError';
26
+ this.status = params.status;
27
+ this.statusText = params.statusText;
28
+ this.method = params.method;
29
+ this.url = params.url;
30
+ this.body = params.body;
31
+ }
32
32
  }
33
33
 
34
34
  export class ApiFetcher {
35
- constructor(
36
- private readonly API_BASE_URL: string,
37
- private readonly version: ValidAPIVersion,
38
- ) {}
39
-
40
- private getBaseUrl() {
41
- return `${this.API_BASE_URL}/api/v${this.version}`;
42
- }
43
-
44
- private getRequestUrl(path: ValidPath) {
45
- if (typeof path === 'string') {
46
- return `${this.getBaseUrl()}/${path}`;
47
- } else if (path.params) {
48
- const searchParams = new URLSearchParams();
49
- for (const [key, value] of Object.entries(path.params)) {
50
- if (Array.isArray(value)) {
51
- for (const v of value) {
52
- searchParams.append(key, v);
53
- }
54
- } else {
55
- searchParams.append(key, value);
56
- }
57
- }
58
- const paramStr = searchParams.toString();
59
- return `${this.getBaseUrl()}/${path.path}${paramStr ? `?${paramStr}` : ''}`;
60
- } else {
61
- return `${this.getBaseUrl()}/${path.path}`;
62
- }
63
- }
64
-
65
- private async fetch(path: ValidPath, method: ValidMethod, jwt: string, options?: RequestOptions) {
66
- const requestUrl = this.getRequestUrl(path);
67
-
68
- const requestOptions: RequestInit = {
69
- method,
70
- headers: {
71
- Authorization: `Bearer ${jwt}`,
72
- 'Content-Type': 'application/json',
73
- 'Code-Storage-Agent': getUserAgent(),
74
- },
75
- };
76
-
77
- if (method !== 'GET' && typeof path !== 'string' && path.body) {
78
- requestOptions.body = JSON.stringify(path.body);
79
- }
80
-
81
- const response = await fetch(requestUrl, requestOptions);
82
-
83
- if (!response.ok) {
84
- const allowed = options?.allowedStatus ?? [];
85
- if (allowed.includes(response.status)) {
86
- return response;
87
- }
88
-
89
- let errorBody: unknown;
90
- let message: string | undefined;
91
- const contentType = response.headers.get('content-type') ?? '';
92
-
93
- try {
94
- if (contentType.includes('application/json')) {
95
- errorBody = await response.json();
96
- } else {
97
- const text = await response.text();
98
- errorBody = text;
99
- }
100
- } catch {
101
- // Fallback to plain text if JSON parse failed after reading body
102
- try {
103
- errorBody = await response.text();
104
- } catch {
105
- errorBody = undefined;
106
- }
107
- }
108
-
109
- if (typeof errorBody === 'string') {
110
- const trimmed = errorBody.trim();
111
- if (trimmed) {
112
- message = trimmed;
113
- }
114
- } else if (errorBody && typeof errorBody === 'object') {
115
- const parsedError = errorEnvelopeSchema.safeParse(errorBody);
116
- if (parsedError.success) {
117
- const trimmed = parsedError.data.error.trim();
118
- if (trimmed) {
119
- message = trimmed;
120
- }
121
- }
122
- }
123
-
124
- throw new ApiError({
125
- message:
126
- message ??
127
- `Request ${method} ${requestUrl} failed with status ${response.status} ${response.statusText}`,
128
- status: response.status,
129
- statusText: response.statusText,
130
- method,
131
- url: requestUrl,
132
- body: errorBody,
133
- });
134
- }
135
- return response;
136
- }
137
-
138
- async get(path: ValidPath, jwt: string, options?: RequestOptions) {
139
- return this.fetch(path, 'GET', jwt, options);
140
- }
141
-
142
- async post(path: ValidPath, jwt: string, options?: RequestOptions) {
143
- return this.fetch(path, 'POST', jwt, options);
144
- }
145
-
146
- async put(path: ValidPath, jwt: string, options?: RequestOptions) {
147
- return this.fetch(path, 'PUT', jwt, options);
148
- }
149
-
150
- async delete(path: ValidPath, jwt: string, options?: RequestOptions) {
151
- return this.fetch(path, 'DELETE', jwt, options);
152
- }
35
+ constructor(
36
+ private readonly API_BASE_URL: string,
37
+ private readonly version: ValidAPIVersion
38
+ ) {}
39
+
40
+ private getBaseUrl() {
41
+ return `${this.API_BASE_URL}/api/v${this.version}`;
42
+ }
43
+
44
+ private getRequestUrl(path: ValidPath) {
45
+ if (typeof path === 'string') {
46
+ return `${this.getBaseUrl()}/${path}`;
47
+ } else if (path.params) {
48
+ const searchParams = new URLSearchParams();
49
+ for (const [key, value] of Object.entries(path.params)) {
50
+ if (Array.isArray(value)) {
51
+ for (const v of value) {
52
+ searchParams.append(key, v);
53
+ }
54
+ } else {
55
+ searchParams.append(key, value);
56
+ }
57
+ }
58
+ const paramStr = searchParams.toString();
59
+ return `${this.getBaseUrl()}/${path.path}${paramStr ? `?${paramStr}` : ''}`;
60
+ } else {
61
+ return `${this.getBaseUrl()}/${path.path}`;
62
+ }
63
+ }
64
+
65
+ private async fetch(
66
+ path: ValidPath,
67
+ method: ValidMethod,
68
+ jwt: string,
69
+ options?: RequestOptions
70
+ ) {
71
+ const requestUrl = this.getRequestUrl(path);
72
+
73
+ const requestOptions: RequestInit = {
74
+ method,
75
+ headers: {
76
+ Authorization: `Bearer ${jwt}`,
77
+ 'Content-Type': 'application/json',
78
+ 'Code-Storage-Agent': getUserAgent(),
79
+ },
80
+ };
81
+
82
+ if (method !== 'GET' && typeof path !== 'string' && path.body) {
83
+ requestOptions.body = JSON.stringify(path.body);
84
+ }
85
+
86
+ const response = await fetch(requestUrl, requestOptions);
87
+
88
+ if (!response.ok) {
89
+ const allowed = options?.allowedStatus ?? [];
90
+ if (allowed.includes(response.status)) {
91
+ return response;
92
+ }
93
+
94
+ let errorBody: unknown;
95
+ let message: string | undefined;
96
+ const contentType = response.headers.get('content-type') ?? '';
97
+
98
+ try {
99
+ if (contentType.includes('application/json')) {
100
+ errorBody = await response.json();
101
+ } else {
102
+ const text = await response.text();
103
+ errorBody = text;
104
+ }
105
+ } catch {
106
+ // Fallback to plain text if JSON parse failed after reading body
107
+ try {
108
+ errorBody = await response.text();
109
+ } catch {
110
+ errorBody = undefined;
111
+ }
112
+ }
113
+
114
+ if (typeof errorBody === 'string') {
115
+ const trimmed = errorBody.trim();
116
+ if (trimmed) {
117
+ message = trimmed;
118
+ }
119
+ } else if (errorBody && typeof errorBody === 'object') {
120
+ const parsedError = errorEnvelopeSchema.safeParse(errorBody);
121
+ if (parsedError.success) {
122
+ const trimmed = parsedError.data.error.trim();
123
+ if (trimmed) {
124
+ message = trimmed;
125
+ }
126
+ }
127
+ }
128
+
129
+ throw new ApiError({
130
+ message:
131
+ message ??
132
+ `Request ${method} ${requestUrl} failed with status ${response.status} ${response.statusText}`,
133
+ status: response.status,
134
+ statusText: response.statusText,
135
+ method,
136
+ url: requestUrl,
137
+ body: errorBody,
138
+ });
139
+ }
140
+ return response;
141
+ }
142
+
143
+ async get(path: ValidPath, jwt: string, options?: RequestOptions) {
144
+ return this.fetch(path, 'GET', jwt, options);
145
+ }
146
+
147
+ async post(path: ValidPath, jwt: string, options?: RequestOptions) {
148
+ return this.fetch(path, 'POST', jwt, options);
149
+ }
150
+
151
+ async put(path: ValidPath, jwt: string, options?: RequestOptions) {
152
+ return this.fetch(path, 'PUT', jwt, options);
153
+ }
154
+
155
+ async delete(path: ValidPath, jwt: string, options?: RequestOptions) {
156
+ return this.fetch(path, 'DELETE', jwt, options);
157
+ }
153
158
  }