@giveitsmaller/sdk 0.2.2 → 0.2.3
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/client.d.ts +11 -0
- package/dist/client.js +53 -15
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +11 -6
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -19,6 +19,17 @@ export declare class GislClient {
|
|
|
19
19
|
*/
|
|
20
20
|
uploadFile(file: string | Blob, options?: UploadOptions): Promise<UploadResponse>;
|
|
21
21
|
private singleUpload;
|
|
22
|
+
/**
|
|
23
|
+
* Direct-to-S3 multipart upload for files above the threshold.
|
|
24
|
+
*
|
|
25
|
+
* The /multipart/complete response (MultipartCompleteResponse) only carries
|
|
26
|
+
* { upload_id, status }. The server's upload_id is the same UUID callers
|
|
27
|
+
* pass as file_id to POST /api/workflows — so fileId is synthesised from
|
|
28
|
+
* upload_id and a full UploadResponse is returned to keep the public
|
|
29
|
+
* uploadFile() API uniform across single and multipart paths. The mimeType
|
|
30
|
+
* comes from the initiate response's first-chunk detection; for authoritative
|
|
31
|
+
* post-upload metadata callers should use getMetadata(fileId).
|
|
32
|
+
*/
|
|
22
33
|
private multipartUpload;
|
|
23
34
|
/**
|
|
24
35
|
* Create a new workflow.
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
|
-
import { UploadResponseFromJSON, MultipartInitiateResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, } from '@giveitsmaller/contracts/openapi';
|
|
3
|
+
import { UploadResponseFromJSON, MultipartInitiateResponseFromJSON, MultipartCompleteResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, } from '@giveitsmaller/contracts/openapi';
|
|
4
4
|
import { GislApiError, GislError, GislTimeoutError, GislValidationError } from './errors.js';
|
|
5
5
|
import { parseSseStream } from './sse.js';
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -19,6 +19,13 @@ const TERMINAL_STATUSES = new Set([
|
|
|
19
19
|
WorkflowStatus.failed,
|
|
20
20
|
WorkflowStatus.partially_failed,
|
|
21
21
|
]);
|
|
22
|
+
function isValidationDetails(value) {
|
|
23
|
+
return (Array.isArray(value) &&
|
|
24
|
+
value.every((el) => typeof el === 'object' &&
|
|
25
|
+
el !== null &&
|
|
26
|
+
typeof el.field === 'string' &&
|
|
27
|
+
typeof el.message === 'string'));
|
|
28
|
+
}
|
|
22
29
|
export class GislClient {
|
|
23
30
|
baseUrl;
|
|
24
31
|
headers;
|
|
@@ -78,27 +85,34 @@ export class GislClient {
|
|
|
78
85
|
return this.handleResponse(response, path, opts.deserialize);
|
|
79
86
|
}
|
|
80
87
|
async handleResponse(response, path, deserialize) {
|
|
81
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
82
|
-
|
|
88
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
89
|
+
const isJsonContent = contentType.includes('application/json') || contentType.includes('+json');
|
|
90
|
+
if (!isJsonContent) {
|
|
83
91
|
if (!response.ok) {
|
|
84
|
-
throw new GislApiError(response.status,
|
|
92
|
+
throw new GislApiError(response.status, 'Non-JSON response', path);
|
|
85
93
|
}
|
|
86
94
|
return undefined;
|
|
87
95
|
}
|
|
88
|
-
|
|
96
|
+
let json;
|
|
97
|
+
try {
|
|
98
|
+
json = await response.json();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new GislApiError(response.status, 'Invalid JSON response', path);
|
|
102
|
+
}
|
|
89
103
|
// Schema endpoint returns raw JSON (no envelope)
|
|
90
104
|
if (path === '/api/operations/schema') {
|
|
91
105
|
if (!response.ok) {
|
|
92
|
-
throw new GislApiError(response.status, json.error ?? 'Unknown error');
|
|
106
|
+
throw new GislApiError(response.status, json.error ?? 'Unknown error', path);
|
|
93
107
|
}
|
|
94
108
|
return deserialize ? deserialize(json) : json;
|
|
95
109
|
}
|
|
96
110
|
// Standard envelope: { success, data } or { success, error, details }
|
|
97
111
|
if (!response.ok || json.success === false) {
|
|
98
|
-
if (
|
|
99
|
-
throw new GislValidationError(response.status, json.error ?? 'Validation error', json.details);
|
|
112
|
+
if (isValidationDetails(json.details)) {
|
|
113
|
+
throw new GislValidationError(response.status, json.error ?? 'Validation error', json.details, path);
|
|
100
114
|
}
|
|
101
|
-
throw new GislApiError(response.status, json.error ?? 'Unknown error');
|
|
115
|
+
throw new GislApiError(response.status, json.error ?? 'Unknown error', path, json.details);
|
|
102
116
|
}
|
|
103
117
|
const data = json.data ?? json;
|
|
104
118
|
return deserialize ? deserialize(data) : data;
|
|
@@ -143,6 +157,17 @@ export class GislClient {
|
|
|
143
157
|
deserialize: UploadResponseFromJSON,
|
|
144
158
|
});
|
|
145
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Direct-to-S3 multipart upload for files above the threshold.
|
|
162
|
+
*
|
|
163
|
+
* The /multipart/complete response (MultipartCompleteResponse) only carries
|
|
164
|
+
* { upload_id, status }. The server's upload_id is the same UUID callers
|
|
165
|
+
* pass as file_id to POST /api/workflows — so fileId is synthesised from
|
|
166
|
+
* upload_id and a full UploadResponse is returned to keep the public
|
|
167
|
+
* uploadFile() API uniform across single and multipart paths. The mimeType
|
|
168
|
+
* comes from the initiate response's first-chunk detection; for authoritative
|
|
169
|
+
* post-upload metadata callers should use getMetadata(fileId).
|
|
170
|
+
*/
|
|
146
171
|
async multipartUpload(blob, fileName, totalSize, options) {
|
|
147
172
|
// Step 1: Initiate with first chunk
|
|
148
173
|
const firstChunkSize = Math.min(totalSize, DEFAULT_MULTIPART_FIRST_CHUNK_SIZE);
|
|
@@ -192,15 +217,27 @@ export class GislClient {
|
|
|
192
217
|
}
|
|
193
218
|
});
|
|
194
219
|
await Promise.all(workers);
|
|
195
|
-
// Step 3: Complete multipart upload
|
|
220
|
+
// Step 3: Complete multipart upload.
|
|
196
221
|
etags.sort((a, b) => a.part_number - b.part_number);
|
|
197
|
-
|
|
222
|
+
const completeResp = await this.request('POST', '/api/uploads/multipart/complete', {
|
|
198
223
|
body: {
|
|
199
|
-
|
|
224
|
+
upload_id: initResponse.uploadId,
|
|
200
225
|
parts: etags,
|
|
201
226
|
},
|
|
202
|
-
deserialize:
|
|
227
|
+
deserialize: MultipartCompleteResponseFromJSON,
|
|
203
228
|
});
|
|
229
|
+
// Defensive: the status enum currently has only 'completed', but guard
|
|
230
|
+
// against future expansion so an unexpected terminal state doesn't pass
|
|
231
|
+
// as a successful upload.
|
|
232
|
+
if (completeResp.status !== 'completed') {
|
|
233
|
+
throw new GislError(`Multipart upload completed with unexpected status: ${completeResp.status}`);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
fileId: completeResp.uploadId,
|
|
237
|
+
originalName: fileName,
|
|
238
|
+
mimeType: initResponse.mimeType,
|
|
239
|
+
sizeBytes: blob.size,
|
|
240
|
+
};
|
|
204
241
|
}
|
|
205
242
|
// -----------------------------------------------------------------------
|
|
206
243
|
// Workflows
|
|
@@ -253,9 +290,10 @@ export class GislClient {
|
|
|
253
290
|
* Stream SSE events for a workflow. Returns an async iterable.
|
|
254
291
|
*/
|
|
255
292
|
async streamEvents(workflowId) {
|
|
256
|
-
const
|
|
293
|
+
const eventsPath = `/api/workflows/${encodeURIComponent(workflowId)}/events`;
|
|
294
|
+
const response = await this.request('GET', eventsPath, { rawResponse: true });
|
|
257
295
|
if (!response.ok) {
|
|
258
|
-
await this.handleResponse(response,
|
|
296
|
+
await this.handleResponse(response, eventsPath);
|
|
259
297
|
}
|
|
260
298
|
return parseSseStream(response);
|
|
261
299
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export declare class GislError extends Error {
|
|
|
4
4
|
export declare class GislApiError extends GislError {
|
|
5
5
|
readonly statusCode: number;
|
|
6
6
|
readonly errorMessage: string;
|
|
7
|
-
|
|
7
|
+
readonly path?: string;
|
|
8
|
+
readonly details?: unknown;
|
|
9
|
+
constructor(statusCode: number, errorMessage: string, path?: string, details?: unknown);
|
|
8
10
|
}
|
|
9
11
|
export declare class GislValidationError extends GislApiError {
|
|
10
12
|
readonly details: Array<{
|
|
@@ -14,7 +16,7 @@ export declare class GislValidationError extends GislApiError {
|
|
|
14
16
|
constructor(statusCode: number, errorMessage: string, details: Array<{
|
|
15
17
|
field: string;
|
|
16
18
|
message: string;
|
|
17
|
-
}
|
|
19
|
+
}>, path?: string);
|
|
18
20
|
}
|
|
19
21
|
export declare class GislTimeoutError extends GislError {
|
|
20
22
|
constructor(message: string);
|
package/dist/errors.js
CHANGED
|
@@ -7,19 +7,24 @@ export class GislError extends Error {
|
|
|
7
7
|
export class GislApiError extends GislError {
|
|
8
8
|
statusCode;
|
|
9
9
|
errorMessage;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
path;
|
|
11
|
+
details;
|
|
12
|
+
constructor(statusCode, errorMessage, path, details) {
|
|
13
|
+
const prefix = path
|
|
14
|
+
? `API error ${statusCode} at ${path}`
|
|
15
|
+
: `API error ${statusCode}`;
|
|
16
|
+
super(`${prefix}: ${errorMessage}`);
|
|
12
17
|
this.name = 'GislApiError';
|
|
13
18
|
this.statusCode = statusCode;
|
|
14
19
|
this.errorMessage = errorMessage;
|
|
20
|
+
this.path = path;
|
|
21
|
+
this.details = details;
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
24
|
export class GislValidationError extends GislApiError {
|
|
18
|
-
details
|
|
19
|
-
|
|
20
|
-
super(statusCode, errorMessage);
|
|
25
|
+
constructor(statusCode, errorMessage, details, path) {
|
|
26
|
+
super(statusCode, errorMessage, path, details);
|
|
21
27
|
this.name = 'GislValidationError';
|
|
22
|
-
this.details = details;
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
export class GislTimeoutError extends GislError {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giveitsmaller/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"node": ">=18"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@giveitsmaller/contracts": "^0.
|
|
22
|
+
"@giveitsmaller/contracts": "^0.2.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^22",
|