@giveitsmaller/sdk 0.2.1 → 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 +12 -0
- package/dist/client.js +67 -20
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +11 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -3
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UploadResponse, WorkflowCreateResponse, WorkflowStatusResponse, WorkflowDownloadResponse, MetadataResponse, OperationsSchemaResponse, RetryResponse } from '@giveitsmaller/contracts/openapi';
|
|
2
2
|
import type { GislClientConfig, GislSseEvent, UploadOptions, WaitOptions, WorkflowCreatePayload } from './types.js';
|
|
3
|
+
export declare const DEFAULT_MULTIPART_FIRST_CHUNK_SIZE: number;
|
|
3
4
|
export declare class GislClient {
|
|
4
5
|
private readonly baseUrl;
|
|
5
6
|
private readonly headers;
|
|
@@ -18,6 +19,17 @@ export declare class GislClient {
|
|
|
18
19
|
*/
|
|
19
20
|
uploadFile(file: string | Blob, options?: UploadOptions): Promise<UploadResponse>;
|
|
20
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
|
+
*/
|
|
21
33
|
private multipartUpload;
|
|
22
34
|
/**
|
|
23
35
|
* Create a new workflow.
|
package/dist/client.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
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;
|
|
7
7
|
const DEFAULT_MULTIPART_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
|
8
8
|
const DEFAULT_MULTIPART_CONCURRENCY = 4;
|
|
9
|
+
// Fixed per contract (compression_contracts/openapi/api.yaml:134). The server
|
|
10
|
+
// uses the first chunk for MIME detection + throughput measurement and stores
|
|
11
|
+
// it as S3 multipart part 1. Must NOT be derived from multipartThreshold —
|
|
12
|
+
// that is the "use multipart above this size" routing threshold, a separate
|
|
13
|
+
// concept. Conflating them caused the /api/uploads/multipart/initiate 413.
|
|
14
|
+
export const DEFAULT_MULTIPART_FIRST_CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB
|
|
9
15
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
10
16
|
const DEFAULT_POLL_TIMEOUT_MS = 300_000; // 5 min
|
|
11
17
|
const TERMINAL_STATUSES = new Set([
|
|
@@ -13,6 +19,13 @@ const TERMINAL_STATUSES = new Set([
|
|
|
13
19
|
WorkflowStatus.failed,
|
|
14
20
|
WorkflowStatus.partially_failed,
|
|
15
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
|
+
}
|
|
16
29
|
export class GislClient {
|
|
17
30
|
baseUrl;
|
|
18
31
|
headers;
|
|
@@ -22,7 +35,10 @@ export class GislClient {
|
|
|
22
35
|
constructor(config) {
|
|
23
36
|
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
24
37
|
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
25
|
-
|
|
38
|
+
// Floor the threshold at the first-chunk size: the multipart initiate
|
|
39
|
+
// must always carry an 8MB chunk, so routing a sub-8MB file into the
|
|
40
|
+
// multipart path would violate the contract.
|
|
41
|
+
this.multipartThreshold = Math.max(config.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD, DEFAULT_MULTIPART_FIRST_CHUNK_SIZE);
|
|
26
42
|
this.multipartConcurrency = config.multipartConcurrency ?? DEFAULT_MULTIPART_CONCURRENCY;
|
|
27
43
|
this.headers = { ...config.headers };
|
|
28
44
|
if (config.apiKey) {
|
|
@@ -69,27 +85,34 @@ export class GislClient {
|
|
|
69
85
|
return this.handleResponse(response, path, opts.deserialize);
|
|
70
86
|
}
|
|
71
87
|
async handleResponse(response, path, deserialize) {
|
|
72
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
73
|
-
|
|
88
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
89
|
+
const isJsonContent = contentType.includes('application/json') || contentType.includes('+json');
|
|
90
|
+
if (!isJsonContent) {
|
|
74
91
|
if (!response.ok) {
|
|
75
|
-
throw new GislApiError(response.status,
|
|
92
|
+
throw new GislApiError(response.status, 'Non-JSON response', path);
|
|
76
93
|
}
|
|
77
94
|
return undefined;
|
|
78
95
|
}
|
|
79
|
-
|
|
96
|
+
let json;
|
|
97
|
+
try {
|
|
98
|
+
json = await response.json();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new GislApiError(response.status, 'Invalid JSON response', path);
|
|
102
|
+
}
|
|
80
103
|
// Schema endpoint returns raw JSON (no envelope)
|
|
81
104
|
if (path === '/api/operations/schema') {
|
|
82
105
|
if (!response.ok) {
|
|
83
|
-
throw new GislApiError(response.status, json.error ?? 'Unknown error');
|
|
106
|
+
throw new GislApiError(response.status, json.error ?? 'Unknown error', path);
|
|
84
107
|
}
|
|
85
108
|
return deserialize ? deserialize(json) : json;
|
|
86
109
|
}
|
|
87
110
|
// Standard envelope: { success, data } or { success, error, details }
|
|
88
111
|
if (!response.ok || json.success === false) {
|
|
89
|
-
if (
|
|
90
|
-
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);
|
|
91
114
|
}
|
|
92
|
-
throw new GislApiError(response.status, json.error ?? 'Unknown error');
|
|
115
|
+
throw new GislApiError(response.status, json.error ?? 'Unknown error', path, json.details);
|
|
93
116
|
}
|
|
94
117
|
const data = json.data ?? json;
|
|
95
118
|
return deserialize ? deserialize(data) : data;
|
|
@@ -134,14 +157,25 @@ export class GislClient {
|
|
|
134
157
|
deserialize: UploadResponseFromJSON,
|
|
135
158
|
});
|
|
136
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
|
+
*/
|
|
137
171
|
async multipartUpload(blob, fileName, totalSize, options) {
|
|
138
172
|
// Step 1: Initiate with first chunk
|
|
139
|
-
const firstChunkSize = Math.min(totalSize,
|
|
173
|
+
const firstChunkSize = Math.min(totalSize, DEFAULT_MULTIPART_FIRST_CHUNK_SIZE);
|
|
140
174
|
const firstChunk = blob.slice(0, firstChunkSize);
|
|
141
175
|
const initiateForm = new FormData();
|
|
142
|
-
initiateForm.append('
|
|
143
|
-
initiateForm.append('
|
|
144
|
-
initiateForm.append('
|
|
176
|
+
initiateForm.append('file', firstChunk, fileName);
|
|
177
|
+
initiateForm.append('filename', fileName);
|
|
178
|
+
initiateForm.append('total_size', totalSize.toString());
|
|
145
179
|
const initResponse = await this.request('POST', '/api/uploads/multipart/initiate', {
|
|
146
180
|
body: initiateForm,
|
|
147
181
|
json: false,
|
|
@@ -183,15 +217,27 @@ export class GislClient {
|
|
|
183
217
|
}
|
|
184
218
|
});
|
|
185
219
|
await Promise.all(workers);
|
|
186
|
-
// Step 3: Complete multipart upload
|
|
220
|
+
// Step 3: Complete multipart upload.
|
|
187
221
|
etags.sort((a, b) => a.part_number - b.part_number);
|
|
188
|
-
|
|
222
|
+
const completeResp = await this.request('POST', '/api/uploads/multipart/complete', {
|
|
189
223
|
body: {
|
|
190
|
-
|
|
224
|
+
upload_id: initResponse.uploadId,
|
|
191
225
|
parts: etags,
|
|
192
226
|
},
|
|
193
|
-
deserialize:
|
|
227
|
+
deserialize: MultipartCompleteResponseFromJSON,
|
|
194
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
|
+
};
|
|
195
241
|
}
|
|
196
242
|
// -----------------------------------------------------------------------
|
|
197
243
|
// Workflows
|
|
@@ -244,9 +290,10 @@ export class GislClient {
|
|
|
244
290
|
* Stream SSE events for a workflow. Returns an async iterable.
|
|
245
291
|
*/
|
|
246
292
|
async streamEvents(workflowId) {
|
|
247
|
-
const
|
|
293
|
+
const eventsPath = `/api/workflows/${encodeURIComponent(workflowId)}/events`;
|
|
294
|
+
const response = await this.request('GET', eventsPath, { rawResponse: true });
|
|
248
295
|
if (!response.ok) {
|
|
249
|
-
await this.handleResponse(response,
|
|
296
|
+
await this.handleResponse(response, eventsPath);
|
|
250
297
|
}
|
|
251
298
|
return parseSseStream(response);
|
|
252
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/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { GislClient } from './client.js';
|
|
1
|
+
export { GislClient, DEFAULT_MULTIPART_FIRST_CHUNK_SIZE } from './client.js';
|
|
2
2
|
export { verifyWebhook } from './webhook.js';
|
|
3
3
|
export { parseSseStream } from './sse.js';
|
|
4
4
|
export type { GislClientConfig, GislSseEvent, UploadOptions, WaitOptions, WorkflowCreatePayload, OperationDef, FileJobPayload, SourceJobPayload, InputsJobPayload, JobDefinitionPayload, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SDK classes and functions
|
|
2
|
-
export { GislClient } from './client.js';
|
|
2
|
+
export { GislClient, DEFAULT_MULTIPART_FIRST_CHUNK_SIZE } from './client.js';
|
|
3
3
|
export { verifyWebhook } from './webhook.js';
|
|
4
4
|
export { parseSseStream } from './sse.js';
|
|
5
5
|
export { fileJob, sourceJob, inputsJob } from './types.js';
|
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,12 +19,13 @@
|
|
|
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",
|
|
26
26
|
"typescript": "^5.7",
|
|
27
|
-
"vitest": "^3.1"
|
|
27
|
+
"vitest": "^3.1",
|
|
28
|
+
"yaml": "^2.6"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"build": "tsc",
|