@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 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
- if (!contentType.includes('application/json')) {
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, `Non-JSON error from ${path}`);
92
+ throw new GislApiError(response.status, 'Non-JSON response', path);
85
93
  }
86
94
  return undefined;
87
95
  }
88
- const json = await response.json();
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 (json.details && Array.isArray(json.details)) {
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
- return this.request('POST', '/api/uploads/multipart/complete', {
222
+ const completeResp = await this.request('POST', '/api/uploads/multipart/complete', {
198
223
  body: {
199
- file_id: initResponse.fileId,
224
+ upload_id: initResponse.uploadId,
200
225
  parts: etags,
201
226
  },
202
- deserialize: UploadResponseFromJSON,
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 response = await this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/events`, { rawResponse: true });
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, `/api/workflows/${workflowId}/events`);
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
- constructor(statusCode: number, errorMessage: string);
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
- constructor(statusCode, errorMessage) {
11
- super(`API error ${statusCode}: ${errorMessage}`);
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
- constructor(statusCode, errorMessage, details) {
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.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.1.0"
22
+ "@giveitsmaller/contracts": "^0.2.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^22",