@unisource/sdk 0.1.2 → 0.2.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/dist/index.d.mts +348 -44
- package/dist/index.d.ts +348 -44
- package/dist/index.mjs +274 -53
- package/package.json +2 -2
- package/src/client.ts +233 -0
- package/src/fileRecords.ts +87 -0
- package/src/folders.ts +84 -0
- package/src/index.ts +106 -125
- package/src/primitives.ts +25 -0
- package/src/services.ts +69 -0
- package/src/uploadDestination.ts +4 -0
- package/src/uploads.ts +90 -0
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
//#region src/
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const positiveIntegerSchema = z.number().int().positive();
|
|
2
|
+
//#region src/primitives.ts
|
|
3
|
+
const nonEmptyString = z.string().trim().min(1);
|
|
4
|
+
const positiveInt = z.number().int().positive();
|
|
5
|
+
const unixTimestamp = z.number().int().nonnegative();
|
|
7
6
|
const uploadDestinationSchema = z.enum(["r2", "appwrite"]);
|
|
8
7
|
const uploadStatusSchema = z.enum([
|
|
9
8
|
"pending",
|
|
@@ -11,84 +10,306 @@ const uploadStatusSchema = z.enum([
|
|
|
11
10
|
"failed"
|
|
12
11
|
]);
|
|
13
12
|
const apiErrorSchema = z.object({
|
|
14
|
-
error:
|
|
15
|
-
message:
|
|
13
|
+
error: nonEmptyString,
|
|
14
|
+
message: nonEmptyString
|
|
16
15
|
});
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/uploads.ts
|
|
17
18
|
const uploadR2InitRequestSchema = z.object({
|
|
18
|
-
filename:
|
|
19
|
-
size:
|
|
20
|
-
mime_type:
|
|
21
|
-
bucket: nonEmptyStringSchema.optional()
|
|
19
|
+
filename: nonEmptyString,
|
|
20
|
+
size: positiveInt,
|
|
21
|
+
mime_type: nonEmptyString
|
|
22
22
|
});
|
|
23
23
|
const uploadR2InitResponseSchema = z.object({
|
|
24
|
-
upload_id:
|
|
24
|
+
upload_id: nonEmptyString,
|
|
25
25
|
destination: z.literal("r2"),
|
|
26
26
|
presigned_url: z.string().url(),
|
|
27
|
-
storage_key:
|
|
28
|
-
bucket:
|
|
29
|
-
expires_at:
|
|
27
|
+
storage_key: nonEmptyString,
|
|
28
|
+
bucket: nonEmptyString,
|
|
29
|
+
expires_at: positiveInt
|
|
30
30
|
});
|
|
31
31
|
const uploadAppwriteInitRequestSchema = z.object({
|
|
32
|
-
filename:
|
|
33
|
-
size:
|
|
34
|
-
mime_type:
|
|
32
|
+
filename: nonEmptyString,
|
|
33
|
+
size: positiveInt,
|
|
34
|
+
mime_type: nonEmptyString
|
|
35
35
|
});
|
|
36
36
|
const uploadAppwriteInitResponseSchema = z.object({
|
|
37
|
-
upload_id:
|
|
37
|
+
upload_id: nonEmptyString,
|
|
38
38
|
destination: z.literal("appwrite"),
|
|
39
39
|
appwrite_endpoint: z.string().url(),
|
|
40
|
-
appwrite_project_id:
|
|
41
|
-
appwrite_bucket_id:
|
|
42
|
-
file_id:
|
|
43
|
-
expires_at:
|
|
40
|
+
appwrite_project_id: nonEmptyString,
|
|
41
|
+
appwrite_bucket_id: nonEmptyString,
|
|
42
|
+
file_id: nonEmptyString,
|
|
43
|
+
expires_at: positiveInt
|
|
44
44
|
});
|
|
45
|
-
const uploadLifecycleRequestSchema = z.object({ upload_id:
|
|
45
|
+
const uploadLifecycleRequestSchema = z.object({ upload_id: nonEmptyString });
|
|
46
46
|
const uploadCompleteResponseSchema = z.object({
|
|
47
47
|
success: z.literal(true),
|
|
48
|
-
upload_id:
|
|
48
|
+
upload_id: nonEmptyString,
|
|
49
49
|
status: z.literal("completed")
|
|
50
50
|
});
|
|
51
51
|
const uploadFailResponseSchema = z.object({
|
|
52
52
|
success: z.literal(true),
|
|
53
|
-
upload_id:
|
|
53
|
+
upload_id: nonEmptyString,
|
|
54
54
|
status: z.literal("failed")
|
|
55
55
|
});
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const FILES_DEFAULT_LIMIT = 25;
|
|
57
|
+
const FILES_MAX_LIMIT = 100;
|
|
58
|
+
/** Raw upload record — returned by admin `/files` endpoints. */
|
|
59
|
+
const uploadRecordSchema = z.object({
|
|
60
|
+
id: nonEmptyString,
|
|
61
|
+
service_id: nonEmptyString,
|
|
62
|
+
user_id: nonEmptyString.nullable(),
|
|
63
|
+
filename: nonEmptyString,
|
|
64
|
+
size: positiveInt,
|
|
65
|
+
mime_type: nonEmptyString,
|
|
61
66
|
destination: uploadDestinationSchema,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
status: z.enum([
|
|
68
|
+
"pending",
|
|
69
|
+
"completed",
|
|
70
|
+
"failed"
|
|
71
|
+
]),
|
|
72
|
+
expires_at: positiveInt,
|
|
73
|
+
created_at: positiveInt,
|
|
74
|
+
updated_at: positiveInt
|
|
75
|
+
});
|
|
76
|
+
const uploadsListResponseSchema = z.object({
|
|
77
|
+
items: z.array(uploadRecordSchema),
|
|
78
|
+
next_cursor: z.string().nullable(),
|
|
79
|
+
limit: positiveInt
|
|
80
|
+
});
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/fileRecords.ts
|
|
83
|
+
/**
|
|
84
|
+
* A confirmed file record owned by a user.
|
|
85
|
+
* Internal fields (storage_key, bucket) are intentionally excluded from the public API.
|
|
86
|
+
*/
|
|
87
|
+
const fileRecordSchema = z.object({
|
|
88
|
+
id: nonEmptyString,
|
|
89
|
+
service_id: nonEmptyString,
|
|
90
|
+
user_id: nonEmptyString,
|
|
91
|
+
folder_id: nonEmptyString.nullable(),
|
|
92
|
+
upload_id: nonEmptyString.nullable(),
|
|
93
|
+
filename: nonEmptyString,
|
|
94
|
+
size: positiveInt,
|
|
95
|
+
mime_type: nonEmptyString,
|
|
96
|
+
storage_destination: uploadDestinationSchema,
|
|
97
|
+
is_trashed: z.boolean(),
|
|
98
|
+
trashed_at: positiveInt.nullable(),
|
|
99
|
+
created_at: positiveInt,
|
|
100
|
+
updated_at: positiveInt
|
|
101
|
+
});
|
|
102
|
+
const fileRecordsListQuerySchema = z.object({
|
|
103
|
+
folder_id: nonEmptyString.nullable().optional(),
|
|
104
|
+
is_trashed: z.boolean().optional(),
|
|
105
|
+
cursor: nonEmptyString.optional(),
|
|
106
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
107
|
+
});
|
|
108
|
+
const fileRecordsListResponseSchema = z.object({
|
|
76
109
|
items: z.array(fileRecordSchema),
|
|
77
110
|
next_cursor: z.string().nullable(),
|
|
78
|
-
limit:
|
|
111
|
+
limit: positiveInt
|
|
79
112
|
});
|
|
80
|
-
const
|
|
113
|
+
const fileRecordDetailResponseSchema = z.object({ file: fileRecordSchema });
|
|
114
|
+
const fileMoveRequestSchema = z.object({ folder_id: nonEmptyString.nullable().optional() });
|
|
81
115
|
const fileDownloadUrlResponseSchema = z.object({
|
|
82
|
-
upload_id:
|
|
116
|
+
upload_id: nonEmptyString,
|
|
83
117
|
destination: uploadDestinationSchema,
|
|
84
118
|
download_url: z.string().url(),
|
|
85
|
-
expires_at:
|
|
119
|
+
expires_at: positiveInt
|
|
86
120
|
});
|
|
87
121
|
const fileDeleteResponseSchema = z.object({
|
|
88
122
|
success: z.literal(true),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
id: nonEmptyString,
|
|
124
|
+
permanent: z.boolean()
|
|
125
|
+
});
|
|
126
|
+
const fileRestoreResponseSchema = z.object({
|
|
127
|
+
success: z.literal(true),
|
|
128
|
+
id: nonEmptyString
|
|
129
|
+
});
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/folders.ts
|
|
132
|
+
const folderSchema = z.object({
|
|
133
|
+
id: nonEmptyString,
|
|
134
|
+
service_id: nonEmptyString,
|
|
135
|
+
user_id: nonEmptyString,
|
|
136
|
+
parent_id: nonEmptyString.nullable(),
|
|
137
|
+
name: nonEmptyString,
|
|
138
|
+
color_tag: z.string().nullable(),
|
|
139
|
+
is_trashed: z.boolean(),
|
|
140
|
+
trashed_at: positiveInt.nullable(),
|
|
141
|
+
created_at: positiveInt,
|
|
142
|
+
updated_at: positiveInt
|
|
92
143
|
});
|
|
144
|
+
const folderListQuerySchema = z.object({
|
|
145
|
+
parent_id: nonEmptyString.nullable().optional(),
|
|
146
|
+
is_trashed: z.boolean().optional(),
|
|
147
|
+
cursor: nonEmptyString.optional(),
|
|
148
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
149
|
+
});
|
|
150
|
+
const folderListResponseSchema = z.object({
|
|
151
|
+
items: z.array(folderSchema),
|
|
152
|
+
next_cursor: z.string().nullable(),
|
|
153
|
+
limit: positiveInt
|
|
154
|
+
});
|
|
155
|
+
const folderCreateRequestSchema = z.object({
|
|
156
|
+
name: nonEmptyString,
|
|
157
|
+
parent_id: nonEmptyString.optional(),
|
|
158
|
+
color_tag: z.string().optional()
|
|
159
|
+
});
|
|
160
|
+
const folderCreateResponseSchema = z.object({ folder: folderSchema });
|
|
161
|
+
const folderUpdateRequestSchema = z.object({
|
|
162
|
+
name: nonEmptyString.optional(),
|
|
163
|
+
color_tag: z.string().nullable().optional()
|
|
164
|
+
}).refine((v) => v.name !== void 0 || v.color_tag !== void 0, { message: "At least one of name or color_tag must be provided" });
|
|
165
|
+
const folderUpdateResponseSchema = z.object({ folder: folderSchema });
|
|
166
|
+
const folderDeleteResponseSchema = z.object({
|
|
167
|
+
success: z.literal(true),
|
|
168
|
+
id: nonEmptyString,
|
|
169
|
+
permanent: z.boolean(),
|
|
170
|
+
folders_deleted: z.number().int().nonnegative().optional()
|
|
171
|
+
});
|
|
172
|
+
const folderRestoreResponseSchema = z.object({
|
|
173
|
+
success: z.literal(true),
|
|
174
|
+
id: nonEmptyString
|
|
175
|
+
});
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/services.ts
|
|
178
|
+
/** Public service info returned to admins. Never exposes secrets. */
|
|
179
|
+
const serviceSchema = z.object({
|
|
180
|
+
id: nonEmptyString,
|
|
181
|
+
name: nonEmptyString,
|
|
182
|
+
max_storage_bytes: positiveInt,
|
|
183
|
+
current_used_bytes: z.number().int().nonnegative(),
|
|
184
|
+
max_file_size_bytes: positiveInt,
|
|
185
|
+
created_at: unixTimestamp
|
|
186
|
+
});
|
|
187
|
+
const serviceDetailResponseSchema = z.object({ service: serviceSchema });
|
|
188
|
+
const serviceUsageResponseSchema = z.object({
|
|
189
|
+
service_id: nonEmptyString,
|
|
190
|
+
max_storage_bytes: positiveInt,
|
|
191
|
+
current_used_bytes: z.number().int().nonnegative(),
|
|
192
|
+
used_percent: z.number().min(0).max(100)
|
|
193
|
+
});
|
|
194
|
+
const auditEventActionSchema = z.enum([
|
|
195
|
+
"upload_completed",
|
|
196
|
+
"file_deleted",
|
|
197
|
+
"folder_deleted",
|
|
198
|
+
"quota_exceeded"
|
|
199
|
+
]);
|
|
200
|
+
const auditEventSchema = z.object({
|
|
201
|
+
id: nonEmptyString,
|
|
202
|
+
service_id: nonEmptyString,
|
|
203
|
+
user_id: nonEmptyString,
|
|
204
|
+
action: auditEventActionSchema,
|
|
205
|
+
resource_type: z.enum([
|
|
206
|
+
"file",
|
|
207
|
+
"folder",
|
|
208
|
+
"service"
|
|
209
|
+
]),
|
|
210
|
+
resource_id: nonEmptyString,
|
|
211
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
212
|
+
ip_address: z.string().nullable(),
|
|
213
|
+
created_at: unixTimestamp
|
|
214
|
+
});
|
|
215
|
+
const auditLogListQuerySchema = z.object({
|
|
216
|
+
user_id: nonEmptyString.optional(),
|
|
217
|
+
action: auditEventActionSchema.optional(),
|
|
218
|
+
resource_type: z.enum([
|
|
219
|
+
"file",
|
|
220
|
+
"folder",
|
|
221
|
+
"service"
|
|
222
|
+
]).optional(),
|
|
223
|
+
cursor: nonEmptyString.optional(),
|
|
224
|
+
limit: z.number().int().min(1).max(200).optional()
|
|
225
|
+
});
|
|
226
|
+
const auditLogListResponseSchema = z.object({
|
|
227
|
+
items: z.array(auditEventSchema),
|
|
228
|
+
next_cursor: z.string().nullable(),
|
|
229
|
+
limit: positiveInt
|
|
230
|
+
});
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/client.ts
|
|
233
|
+
var UnisourceError = class extends Error {
|
|
234
|
+
constructor(message, status, body) {
|
|
235
|
+
super(message);
|
|
236
|
+
this.status = status;
|
|
237
|
+
this.body = body;
|
|
238
|
+
this.name = "UnisourceError";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var UnisourceNetworkError = class extends Error {
|
|
242
|
+
constructor(message, cause) {
|
|
243
|
+
super(message);
|
|
244
|
+
this.cause = cause;
|
|
245
|
+
this.name = "UnisourceNetworkError";
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
async function apiRequest(config, method, path, options = {}) {
|
|
249
|
+
const token = await config.getToken();
|
|
250
|
+
const url = new URL(path, config.baseUrl);
|
|
251
|
+
if (options.query) {
|
|
252
|
+
for (const [key, value] of Object.entries(options.query)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
253
|
+
}
|
|
254
|
+
const headers = { "X-Service-ID": config.serviceId };
|
|
255
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
256
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
257
|
+
let response;
|
|
258
|
+
try {
|
|
259
|
+
response = await fetch(url.toString(), {
|
|
260
|
+
method,
|
|
261
|
+
headers,
|
|
262
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw new UnisourceNetworkError("Network request failed", err);
|
|
266
|
+
}
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
let body;
|
|
269
|
+
try {
|
|
270
|
+
body = await response.json();
|
|
271
|
+
} catch {
|
|
272
|
+
body = {
|
|
273
|
+
error: "Unknown",
|
|
274
|
+
message: response.statusText
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
throw new UnisourceError(body.message, response.status, body);
|
|
278
|
+
}
|
|
279
|
+
return response.json();
|
|
280
|
+
}
|
|
281
|
+
var UnisourceClient = class {
|
|
282
|
+
config;
|
|
283
|
+
constructor(config) {
|
|
284
|
+
this.config = config;
|
|
285
|
+
}
|
|
286
|
+
upload = {
|
|
287
|
+
r2Init: (body) => apiRequest(this.config, "POST", "/upload/r2/init", { body }),
|
|
288
|
+
appwriteInit: (body) => apiRequest(this.config, "POST", "/upload/appwrite/init", { body }),
|
|
289
|
+
complete: (body) => apiRequest(this.config, "POST", "/upload/complete", { body }),
|
|
290
|
+
fail: (body) => apiRequest(this.config, "POST", "/upload/fail", { body })
|
|
291
|
+
};
|
|
292
|
+
myFiles = {
|
|
293
|
+
list: (query) => apiRequest(this.config, "GET", "/my-files", { query }),
|
|
294
|
+
get: (id) => apiRequest(this.config, "GET", `/my-files/${id}`),
|
|
295
|
+
downloadUrl: (id) => apiRequest(this.config, "GET", `/my-files/${id}/download-url`),
|
|
296
|
+
move: (id, body) => apiRequest(this.config, "PATCH", `/my-files/${id}/move`, { body }),
|
|
297
|
+
delete: (id, query) => apiRequest(this.config, "DELETE", `/my-files/${id}`, { query }),
|
|
298
|
+
restore: (id) => apiRequest(this.config, "POST", `/my-files/${id}/restore`)
|
|
299
|
+
};
|
|
300
|
+
folders = {
|
|
301
|
+
list: (query) => apiRequest(this.config, "GET", "/folders", { query }),
|
|
302
|
+
create: (body) => apiRequest(this.config, "POST", "/folders", { body }),
|
|
303
|
+
update: (id, body) => apiRequest(this.config, "PATCH", `/folders/${id}`, { body }),
|
|
304
|
+
delete: (id, query) => apiRequest(this.config, "DELETE", `/folders/${id}`, { query }),
|
|
305
|
+
restore: (id) => apiRequest(this.config, "POST", `/folders/${id}/restore`)
|
|
306
|
+
};
|
|
307
|
+
admin = {
|
|
308
|
+
serviceDetail: () => apiRequest(this.config, "GET", "/admin/service"),
|
|
309
|
+
usage: () => apiRequest(this.config, "GET", "/admin/service/usage"),
|
|
310
|
+
listUploads: (query) => apiRequest(this.config, "GET", "/files", { query }),
|
|
311
|
+
auditLog: (query) => apiRequest(this.config, "GET", "/admin/audit-log", { query })
|
|
312
|
+
};
|
|
313
|
+
};
|
|
93
314
|
//#endregion
|
|
94
|
-
export { FILES_DEFAULT_LIMIT, FILES_MAX_LIMIT, apiErrorSchema,
|
|
315
|
+
export { FILES_DEFAULT_LIMIT, FILES_MAX_LIMIT, UnisourceClient, UnisourceError, UnisourceNetworkError, apiErrorSchema, auditEventActionSchema, auditEventSchema, auditLogListQuerySchema, auditLogListResponseSchema, fileDeleteResponseSchema, fileDownloadUrlResponseSchema, fileMoveRequestSchema, fileRecordDetailResponseSchema, fileRecordSchema, fileRecordsListQuerySchema, fileRecordsListResponseSchema, fileRestoreResponseSchema, folderCreateRequestSchema, folderCreateResponseSchema, folderDeleteResponseSchema, folderListQuerySchema, folderListResponseSchema, folderRestoreResponseSchema, folderSchema, folderUpdateRequestSchema, folderUpdateResponseSchema, nonEmptyString, positiveInt, serviceDetailResponseSchema, serviceSchema, serviceUsageResponseSchema, unixTimestamp, uploadAppwriteInitRequestSchema, uploadAppwriteInitResponseSchema, uploadCompleteResponseSchema, uploadDestinationSchema, uploadFailResponseSchema, uploadLifecycleRequestSchema, uploadR2InitRequestSchema, uploadR2InitResponseSchema, uploadRecordSchema, uploadStatusSchema, uploadsListResponseSchema };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@unisource/sdk",
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"description": "Wspolne kontrakty danych dla backendu i frontendu UniSource.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"publishConfig": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsdown && node -e \"const fs=require('node:fs'); if (fs.existsSync('dist/index.d.mts')) fs.copyFileSync('dist/index.d.mts','dist/index.d.ts');\"",
|
|
32
32
|
"dev": "tsdown --watch",
|
|
33
|
-
"test": "vitest",
|
|
33
|
+
"test": "pnpm run build && vitest",
|
|
34
34
|
"typecheck": "tsc --noEmit"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { ApiError } from './primitives';
|
|
2
|
+
|
|
3
|
+
// ─── SDK Error classes ────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export class UnisourceError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
message: string,
|
|
8
|
+
public readonly status: number,
|
|
9
|
+
public readonly body: ApiError
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'UnisourceError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class UnisourceNetworkError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
message: string,
|
|
19
|
+
public readonly cause: unknown
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'UnisourceNetworkError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface UnisourceClientConfig {
|
|
29
|
+
/** Base URL of the UniSource API, e.g. https://api.usrc.dev */
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
/** Service identifier — tells the backend which service this client belongs to */
|
|
32
|
+
serviceId: string;
|
|
33
|
+
/**
|
|
34
|
+
* Returns a fresh JWT or API key for each request.
|
|
35
|
+
* Return null/undefined to send unauthenticated requests.
|
|
36
|
+
*/
|
|
37
|
+
getToken: () => string | null | undefined | Promise<string | null | undefined>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Internal fetch helper ────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
async function apiRequest<T>(
|
|
43
|
+
config: UnisourceClientConfig,
|
|
44
|
+
method: string,
|
|
45
|
+
path: string,
|
|
46
|
+
options: { body?: unknown; query?: Record<string, string | number | boolean | undefined | null> } = {}
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
const token = await config.getToken();
|
|
49
|
+
|
|
50
|
+
const url = new URL(path, config.baseUrl);
|
|
51
|
+
if (options.query) {
|
|
52
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
53
|
+
if (value !== undefined && value !== null) {
|
|
54
|
+
url.searchParams.set(key, String(value));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const headers: Record<string, string> = {
|
|
60
|
+
'X-Service-ID': config.serviceId,
|
|
61
|
+
};
|
|
62
|
+
if (token) {
|
|
63
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
64
|
+
}
|
|
65
|
+
if (options.body !== undefined) {
|
|
66
|
+
headers['Content-Type'] = 'application/json';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let response: Response;
|
|
70
|
+
try {
|
|
71
|
+
response = await fetch(url.toString(), {
|
|
72
|
+
method,
|
|
73
|
+
headers,
|
|
74
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new UnisourceNetworkError('Network request failed', err);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
let body: ApiError;
|
|
82
|
+
try {
|
|
83
|
+
body = (await response.json()) as ApiError;
|
|
84
|
+
} catch {
|
|
85
|
+
body = { error: 'Unknown', message: response.statusText };
|
|
86
|
+
}
|
|
87
|
+
throw new UnisourceError(body.message, response.status, body);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return response.json() as Promise<T>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Client class ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
import type {
|
|
96
|
+
UploadR2InitRequest,
|
|
97
|
+
UploadR2InitResponse,
|
|
98
|
+
UploadAppwriteInitRequest,
|
|
99
|
+
UploadAppwriteInitResponse,
|
|
100
|
+
UploadLifecycleRequest,
|
|
101
|
+
UploadCompleteResponse,
|
|
102
|
+
UploadFailResponse,
|
|
103
|
+
UploadsListResponse,
|
|
104
|
+
} from './uploads';
|
|
105
|
+
|
|
106
|
+
import type {
|
|
107
|
+
FileRecord,
|
|
108
|
+
FileRecordsListQuery,
|
|
109
|
+
FileRecordsListResponse,
|
|
110
|
+
FileRecordDetailResponse,
|
|
111
|
+
FileMoveRequest,
|
|
112
|
+
FileDownloadUrlResponse,
|
|
113
|
+
FileDeleteResponse,
|
|
114
|
+
FileRestoreResponse,
|
|
115
|
+
} from './fileRecords';
|
|
116
|
+
|
|
117
|
+
import type {
|
|
118
|
+
FolderListQuery,
|
|
119
|
+
FolderListResponse,
|
|
120
|
+
FolderCreateRequest,
|
|
121
|
+
FolderCreateResponse,
|
|
122
|
+
FolderUpdateRequest,
|
|
123
|
+
FolderUpdateResponse,
|
|
124
|
+
FolderDeleteResponse,
|
|
125
|
+
FolderRestoreResponse,
|
|
126
|
+
} from './folders';
|
|
127
|
+
|
|
128
|
+
import type {
|
|
129
|
+
ServiceDetailResponse,
|
|
130
|
+
ServiceUsageResponse,
|
|
131
|
+
AuditLogListQuery,
|
|
132
|
+
AuditLogListResponse,
|
|
133
|
+
} from './services';
|
|
134
|
+
|
|
135
|
+
export class UnisourceClient {
|
|
136
|
+
private config: UnisourceClientConfig;
|
|
137
|
+
|
|
138
|
+
constructor(config: UnisourceClientConfig) {
|
|
139
|
+
this.config = config;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Upload ────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
readonly upload = {
|
|
145
|
+
/** Initiate an R2 upload — returns a presigned PUT URL */
|
|
146
|
+
r2Init: (body: UploadR2InitRequest): Promise<UploadR2InitResponse> =>
|
|
147
|
+
apiRequest(this.config, 'POST', '/upload/r2/init', { body }),
|
|
148
|
+
|
|
149
|
+
/** Initiate an Appwrite upload — returns credentials for direct SDK upload */
|
|
150
|
+
appwriteInit: (body: UploadAppwriteInitRequest): Promise<UploadAppwriteInitResponse> =>
|
|
151
|
+
apiRequest(this.config, 'POST', '/upload/appwrite/init', { body }),
|
|
152
|
+
|
|
153
|
+
/** Confirm that the file was successfully uploaded to storage */
|
|
154
|
+
complete: (body: UploadLifecycleRequest): Promise<UploadCompleteResponse> =>
|
|
155
|
+
apiRequest(this.config, 'POST', '/upload/complete', { body }),
|
|
156
|
+
|
|
157
|
+
/** Mark an upload as failed and release reserved quota */
|
|
158
|
+
fail: (body: UploadLifecycleRequest): Promise<UploadFailResponse> =>
|
|
159
|
+
apiRequest(this.config, 'POST', '/upload/fail', { body }),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ─── My Files ─────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
readonly myFiles = {
|
|
165
|
+
/** List files owned by the authenticated user */
|
|
166
|
+
list: (query?: FileRecordsListQuery): Promise<FileRecordsListResponse> =>
|
|
167
|
+
apiRequest(this.config, 'GET', '/my-files', { query }),
|
|
168
|
+
|
|
169
|
+
/** Get a single file record */
|
|
170
|
+
get: (id: string): Promise<FileRecordDetailResponse> =>
|
|
171
|
+
apiRequest(this.config, 'GET', `/my-files/${id}`),
|
|
172
|
+
|
|
173
|
+
/** Get a time-limited download URL (never cached by proxy) */
|
|
174
|
+
downloadUrl: (id: string): Promise<FileDownloadUrlResponse> =>
|
|
175
|
+
apiRequest(this.config, 'GET', `/my-files/${id}/download-url`),
|
|
176
|
+
|
|
177
|
+
/** Move file to a different folder (null = root) */
|
|
178
|
+
move: (id: string, body: FileMoveRequest): Promise<{ file: FileRecord }> =>
|
|
179
|
+
apiRequest(this.config, 'PATCH', `/my-files/${id}/move`, { body }),
|
|
180
|
+
|
|
181
|
+
/** Soft-delete (move to trash) or permanently delete */
|
|
182
|
+
delete: (id: string, query?: { permanent?: boolean }): Promise<FileDeleteResponse> =>
|
|
183
|
+
apiRequest(this.config, 'DELETE', `/my-files/${id}`, { query }),
|
|
184
|
+
|
|
185
|
+
/** Restore a file from trash */
|
|
186
|
+
restore: (id: string): Promise<FileRestoreResponse> =>
|
|
187
|
+
apiRequest(this.config, 'POST', `/my-files/${id}/restore`),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// ─── Folders ──────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
readonly folders = {
|
|
193
|
+
/** List folders owned by the authenticated user */
|
|
194
|
+
list: (query?: FolderListQuery): Promise<FolderListResponse> =>
|
|
195
|
+
apiRequest(this.config, 'GET', '/folders', { query }),
|
|
196
|
+
|
|
197
|
+
/** Create a new folder */
|
|
198
|
+
create: (body: FolderCreateRequest): Promise<FolderCreateResponse> =>
|
|
199
|
+
apiRequest(this.config, 'POST', '/folders', { body }),
|
|
200
|
+
|
|
201
|
+
/** Update folder name or color tag */
|
|
202
|
+
update: (id: string, body: FolderUpdateRequest): Promise<FolderUpdateResponse> =>
|
|
203
|
+
apiRequest(this.config, 'PATCH', `/folders/${id}`, { body }),
|
|
204
|
+
|
|
205
|
+
/** Soft-delete or permanently delete a folder and its contents */
|
|
206
|
+
delete: (id: string, query?: { permanent?: boolean }): Promise<FolderDeleteResponse> =>
|
|
207
|
+
apiRequest(this.config, 'DELETE', `/folders/${id}`, { query }),
|
|
208
|
+
|
|
209
|
+
/** Restore a folder from trash */
|
|
210
|
+
restore: (id: string): Promise<FolderRestoreResponse> =>
|
|
211
|
+
apiRequest(this.config, 'POST', `/folders/${id}/restore`),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// ─── Admin ────────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
readonly admin = {
|
|
217
|
+
/** Get service info and quota limits */
|
|
218
|
+
serviceDetail: (): Promise<ServiceDetailResponse> =>
|
|
219
|
+
apiRequest(this.config, 'GET', '/admin/service'),
|
|
220
|
+
|
|
221
|
+
/** Get real-time storage usage for the service */
|
|
222
|
+
usage: (): Promise<ServiceUsageResponse> =>
|
|
223
|
+
apiRequest(this.config, 'GET', '/admin/service/usage'),
|
|
224
|
+
|
|
225
|
+
/** List all uploads (pending/completed/failed) for this service */
|
|
226
|
+
listUploads: (query?: { status?: string; cursor?: string; limit?: number }): Promise<UploadsListResponse> =>
|
|
227
|
+
apiRequest(this.config, 'GET', '/files', { query }),
|
|
228
|
+
|
|
229
|
+
/** List audit log events for this service */
|
|
230
|
+
auditLog: (query?: AuditLogListQuery): Promise<AuditLogListResponse> =>
|
|
231
|
+
apiRequest(this.config, 'GET', '/admin/audit-log', { query }),
|
|
232
|
+
};
|
|
233
|
+
}
|