@giveitsmaller/sdk 0.1.1 → 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/client.d.ts +55 -0
- package/dist/client.js +284 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +30 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/sse.d.ts +11 -0
- package/dist/sse.js +95 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +12 -0
- package/dist/webhook.d.ts +10 -0
- package/dist/webhook.js +33 -0
- package/package.json +10 -4
- package/src/client.ts +0 -401
- package/src/errors.ts +0 -39
- package/src/index.ts +0 -86
- package/src/sse.ts +0 -105
- package/src/types.ts +0 -151
- package/src/webhook.ts +0 -48
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { UploadResponse, WorkflowCreateResponse, WorkflowStatusResponse, WorkflowDownloadResponse, MetadataResponse, OperationsSchemaResponse, RetryResponse } from '@giveitsmaller/contracts/openapi';
|
|
2
|
+
import type { GislClientConfig, GislSseEvent, UploadOptions, WaitOptions, WorkflowCreatePayload } from './types.js';
|
|
3
|
+
export declare class GislClient {
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
private readonly headers;
|
|
6
|
+
private readonly timeoutMs;
|
|
7
|
+
private readonly multipartThreshold;
|
|
8
|
+
private readonly multipartConcurrency;
|
|
9
|
+
constructor(config: GislClientConfig);
|
|
10
|
+
private request;
|
|
11
|
+
private handleResponse;
|
|
12
|
+
/**
|
|
13
|
+
* Upload a file. Automatically uses multipart upload for files exceeding
|
|
14
|
+
* the configured threshold (default 10 MB).
|
|
15
|
+
*
|
|
16
|
+
* @param file File path (string) or a Blob/File instance.
|
|
17
|
+
* @param options Upload options including progress callback.
|
|
18
|
+
*/
|
|
19
|
+
uploadFile(file: string | Blob, options?: UploadOptions): Promise<UploadResponse>;
|
|
20
|
+
private singleUpload;
|
|
21
|
+
private multipartUpload;
|
|
22
|
+
/**
|
|
23
|
+
* Create a new workflow.
|
|
24
|
+
*/
|
|
25
|
+
createWorkflow(payload: WorkflowCreatePayload): Promise<WorkflowCreateResponse>;
|
|
26
|
+
/**
|
|
27
|
+
* Get current workflow status.
|
|
28
|
+
*/
|
|
29
|
+
getWorkflowStatus(workflowId: string): Promise<WorkflowStatusResponse>;
|
|
30
|
+
/**
|
|
31
|
+
* Poll until the workflow reaches a terminal status.
|
|
32
|
+
*/
|
|
33
|
+
waitForWorkflow(workflowId: string, options?: WaitOptions): Promise<WorkflowStatusResponse>;
|
|
34
|
+
/**
|
|
35
|
+
* Get download URLs for a completed workflow.
|
|
36
|
+
*/
|
|
37
|
+
getWorkflowDownloads(workflowId: string): Promise<WorkflowDownloadResponse>;
|
|
38
|
+
/**
|
|
39
|
+
* Stream SSE events for a workflow. Returns an async iterable.
|
|
40
|
+
*/
|
|
41
|
+
streamEvents(workflowId: string): Promise<AsyncGenerator<GislSseEvent>>;
|
|
42
|
+
/**
|
|
43
|
+
* Get metadata for an uploaded file.
|
|
44
|
+
*/
|
|
45
|
+
getMetadata(fileId: string): Promise<MetadataResponse>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the operations schema (available types, options, constraints).
|
|
48
|
+
* This endpoint returns raw JSON (no envelope) and is CDN-cacheable.
|
|
49
|
+
*/
|
|
50
|
+
getSchema(): Promise<OperationsSchemaResponse>;
|
|
51
|
+
/**
|
|
52
|
+
* Retry a failed operation.
|
|
53
|
+
*/
|
|
54
|
+
retryOperation(operationId: string): Promise<RetryResponse>;
|
|
55
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { UploadResponseFromJSON, MultipartInitiateResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, } from '@giveitsmaller/contracts/openapi';
|
|
4
|
+
import { GislApiError, GislError, GislTimeoutError, GislValidationError } from './errors.js';
|
|
5
|
+
import { parseSseStream } from './sse.js';
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const DEFAULT_MULTIPART_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
|
8
|
+
const DEFAULT_MULTIPART_CONCURRENCY = 4;
|
|
9
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
10
|
+
const DEFAULT_POLL_TIMEOUT_MS = 300_000; // 5 min
|
|
11
|
+
const TERMINAL_STATUSES = new Set([
|
|
12
|
+
WorkflowStatus.completed,
|
|
13
|
+
WorkflowStatus.failed,
|
|
14
|
+
WorkflowStatus.partially_failed,
|
|
15
|
+
]);
|
|
16
|
+
export class GislClient {
|
|
17
|
+
baseUrl;
|
|
18
|
+
headers;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
multipartThreshold;
|
|
21
|
+
multipartConcurrency;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
24
|
+
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
25
|
+
this.multipartThreshold = config.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
|
|
26
|
+
this.multipartConcurrency = config.multipartConcurrency ?? DEFAULT_MULTIPART_CONCURRENCY;
|
|
27
|
+
this.headers = { ...config.headers };
|
|
28
|
+
if (config.apiKey) {
|
|
29
|
+
this.headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// -----------------------------------------------------------------------
|
|
33
|
+
// Internal HTTP
|
|
34
|
+
// -----------------------------------------------------------------------
|
|
35
|
+
async request(method, path, opts = {}) {
|
|
36
|
+
const url = `${this.baseUrl}${path}`;
|
|
37
|
+
const headers = { ...this.headers };
|
|
38
|
+
let body;
|
|
39
|
+
if (opts.json !== false && opts.body && !(opts.body instanceof FormData)) {
|
|
40
|
+
headers['Content-Type'] = 'application/json';
|
|
41
|
+
body = JSON.stringify(opts.body);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
body = opts.body;
|
|
45
|
+
}
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await fetch(url, {
|
|
51
|
+
method,
|
|
52
|
+
headers,
|
|
53
|
+
body,
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
59
|
+
throw new GislTimeoutError(`Request to ${method} ${path} timed out after ${this.timeoutMs}ms`);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
if (opts.rawResponse) {
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
return this.handleResponse(response, path, opts.deserialize);
|
|
70
|
+
}
|
|
71
|
+
async handleResponse(response, path, deserialize) {
|
|
72
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
73
|
+
if (!contentType.includes('application/json')) {
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new GislApiError(response.status, `Non-JSON error from ${path}`);
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const json = await response.json();
|
|
80
|
+
// Schema endpoint returns raw JSON (no envelope)
|
|
81
|
+
if (path === '/api/operations/schema') {
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new GislApiError(response.status, json.error ?? 'Unknown error');
|
|
84
|
+
}
|
|
85
|
+
return deserialize ? deserialize(json) : json;
|
|
86
|
+
}
|
|
87
|
+
// Standard envelope: { success, data } or { success, error, details }
|
|
88
|
+
if (!response.ok || json.success === false) {
|
|
89
|
+
if (json.details && Array.isArray(json.details)) {
|
|
90
|
+
throw new GislValidationError(response.status, json.error ?? 'Validation error', json.details);
|
|
91
|
+
}
|
|
92
|
+
throw new GislApiError(response.status, json.error ?? 'Unknown error');
|
|
93
|
+
}
|
|
94
|
+
const data = json.data ?? json;
|
|
95
|
+
return deserialize ? deserialize(data) : data;
|
|
96
|
+
}
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
// Upload
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
/**
|
|
101
|
+
* Upload a file. Automatically uses multipart upload for files exceeding
|
|
102
|
+
* the configured threshold (default 10 MB).
|
|
103
|
+
*
|
|
104
|
+
* @param file File path (string) or a Blob/File instance.
|
|
105
|
+
* @param options Upload options including progress callback.
|
|
106
|
+
*/
|
|
107
|
+
async uploadFile(file, options) {
|
|
108
|
+
let blob;
|
|
109
|
+
let fileName;
|
|
110
|
+
let fileSize;
|
|
111
|
+
if (typeof file === 'string') {
|
|
112
|
+
const stat = statSync(file);
|
|
113
|
+
fileSize = stat.size;
|
|
114
|
+
fileName = basename(file);
|
|
115
|
+
const content = readFileSync(file);
|
|
116
|
+
blob = new Blob([content]);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
blob = file;
|
|
120
|
+
fileName = file.name ?? 'upload';
|
|
121
|
+
fileSize = file.size;
|
|
122
|
+
}
|
|
123
|
+
if (fileSize > this.multipartThreshold) {
|
|
124
|
+
return this.multipartUpload(blob, fileName, fileSize, options);
|
|
125
|
+
}
|
|
126
|
+
return this.singleUpload(blob, fileName);
|
|
127
|
+
}
|
|
128
|
+
async singleUpload(blob, fileName) {
|
|
129
|
+
const form = new FormData();
|
|
130
|
+
form.append('file', blob, fileName);
|
|
131
|
+
return this.request('POST', '/api/uploads', {
|
|
132
|
+
body: form,
|
|
133
|
+
json: false,
|
|
134
|
+
deserialize: UploadResponseFromJSON,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async multipartUpload(blob, fileName, totalSize, options) {
|
|
138
|
+
// Step 1: Initiate with first chunk
|
|
139
|
+
const firstChunkSize = Math.min(totalSize, this.multipartThreshold);
|
|
140
|
+
const firstChunk = blob.slice(0, firstChunkSize);
|
|
141
|
+
const initiateForm = new FormData();
|
|
142
|
+
initiateForm.append('chunk', firstChunk, fileName);
|
|
143
|
+
initiateForm.append('original_name', fileName);
|
|
144
|
+
initiateForm.append('total_size_bytes', totalSize.toString());
|
|
145
|
+
const initResponse = await this.request('POST', '/api/uploads/multipart/initiate', {
|
|
146
|
+
body: initiateForm,
|
|
147
|
+
json: false,
|
|
148
|
+
deserialize: MultipartInitiateResponseFromJSON,
|
|
149
|
+
});
|
|
150
|
+
let uploadedBytes = firstChunkSize;
|
|
151
|
+
options?.onProgress?.(uploadedBytes, totalSize);
|
|
152
|
+
// Step 2: Upload remaining chunks to S3 presigned URLs
|
|
153
|
+
const etags = [];
|
|
154
|
+
const presignedUrls = initResponse.presignedUrls;
|
|
155
|
+
const chunkSize = initResponse.recommendedChunkSize;
|
|
156
|
+
const uploadChunk = async (index) => {
|
|
157
|
+
const part = presignedUrls[index];
|
|
158
|
+
const start = firstChunkSize + index * chunkSize;
|
|
159
|
+
const end = Math.min(start + chunkSize, totalSize);
|
|
160
|
+
const chunk = blob.slice(start, end);
|
|
161
|
+
const s3Response = await fetch(part.url, {
|
|
162
|
+
method: 'PUT',
|
|
163
|
+
body: chunk,
|
|
164
|
+
headers: { 'Content-Length': (end - start).toString() },
|
|
165
|
+
});
|
|
166
|
+
if (!s3Response.ok) {
|
|
167
|
+
throw new GislError(`S3 chunk upload failed for part ${part.partNumber}: ${s3Response.status}`);
|
|
168
|
+
}
|
|
169
|
+
const etag = s3Response.headers.get('etag');
|
|
170
|
+
if (!etag) {
|
|
171
|
+
throw new GislError(`S3 response missing ETag for part ${part.partNumber}`);
|
|
172
|
+
}
|
|
173
|
+
etags.push({ part_number: part.partNumber, etag });
|
|
174
|
+
uploadedBytes += end - start;
|
|
175
|
+
options?.onProgress?.(uploadedBytes, totalSize);
|
|
176
|
+
};
|
|
177
|
+
// Upload with concurrency limit
|
|
178
|
+
const queue = [...presignedUrls.keys()];
|
|
179
|
+
const workers = Array.from({ length: Math.min(this.multipartConcurrency, queue.length) }, async () => {
|
|
180
|
+
while (queue.length > 0) {
|
|
181
|
+
const index = queue.shift();
|
|
182
|
+
await uploadChunk(index);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
await Promise.all(workers);
|
|
186
|
+
// Step 3: Complete multipart upload
|
|
187
|
+
etags.sort((a, b) => a.part_number - b.part_number);
|
|
188
|
+
return this.request('POST', '/api/uploads/multipart/complete', {
|
|
189
|
+
body: {
|
|
190
|
+
file_id: initResponse.fileId,
|
|
191
|
+
parts: etags,
|
|
192
|
+
},
|
|
193
|
+
deserialize: UploadResponseFromJSON,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// -----------------------------------------------------------------------
|
|
197
|
+
// Workflows
|
|
198
|
+
// -----------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Create a new workflow.
|
|
201
|
+
*/
|
|
202
|
+
async createWorkflow(payload) {
|
|
203
|
+
return this.request('POST', '/api/workflows', {
|
|
204
|
+
body: payload,
|
|
205
|
+
deserialize: WorkflowCreateResponseFromJSON,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get current workflow status.
|
|
210
|
+
*/
|
|
211
|
+
async getWorkflowStatus(workflowId) {
|
|
212
|
+
return this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/status`, {
|
|
213
|
+
deserialize: WorkflowStatusResponseFromJSON,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Poll until the workflow reaches a terminal status.
|
|
218
|
+
*/
|
|
219
|
+
async waitForWorkflow(workflowId, options) {
|
|
220
|
+
const intervalMs = options?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
221
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
222
|
+
const deadline = Date.now() + timeoutMs;
|
|
223
|
+
while (true) {
|
|
224
|
+
const status = await this.getWorkflowStatus(workflowId);
|
|
225
|
+
options?.onPoll?.(status.status);
|
|
226
|
+
if (TERMINAL_STATUSES.has(status.status)) {
|
|
227
|
+
return status;
|
|
228
|
+
}
|
|
229
|
+
if (Date.now() + intervalMs > deadline) {
|
|
230
|
+
throw new GislTimeoutError(`Workflow ${workflowId} did not complete within ${timeoutMs}ms`);
|
|
231
|
+
}
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get download URLs for a completed workflow.
|
|
237
|
+
*/
|
|
238
|
+
async getWorkflowDownloads(workflowId) {
|
|
239
|
+
return this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/download`, {
|
|
240
|
+
deserialize: WorkflowDownloadResponseFromJSON,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Stream SSE events for a workflow. Returns an async iterable.
|
|
245
|
+
*/
|
|
246
|
+
async streamEvents(workflowId) {
|
|
247
|
+
const response = await this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/events`, { rawResponse: true });
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
await this.handleResponse(response, `/api/workflows/${workflowId}/events`);
|
|
250
|
+
}
|
|
251
|
+
return parseSseStream(response);
|
|
252
|
+
}
|
|
253
|
+
// -----------------------------------------------------------------------
|
|
254
|
+
// File metadata
|
|
255
|
+
// -----------------------------------------------------------------------
|
|
256
|
+
/**
|
|
257
|
+
* Get metadata for an uploaded file.
|
|
258
|
+
*/
|
|
259
|
+
async getMetadata(fileId) {
|
|
260
|
+
return this.request('GET', `/api/uploads/${encodeURIComponent(fileId)}/metadata`, {
|
|
261
|
+
deserialize: MetadataResponseFromJSON,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// -----------------------------------------------------------------------
|
|
265
|
+
// Operations
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
/**
|
|
268
|
+
* Get the operations schema (available types, options, constraints).
|
|
269
|
+
* This endpoint returns raw JSON (no envelope) and is CDN-cacheable.
|
|
270
|
+
*/
|
|
271
|
+
async getSchema() {
|
|
272
|
+
return this.request('GET', '/api/operations/schema', {
|
|
273
|
+
deserialize: OperationsSchemaResponseFromJSON,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Retry a failed operation.
|
|
278
|
+
*/
|
|
279
|
+
async retryOperation(operationId) {
|
|
280
|
+
return this.request('POST', `/api/operations/${encodeURIComponent(operationId)}/retry`, {
|
|
281
|
+
deserialize: RetryResponseFromJSON,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class GislError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class GislApiError extends GislError {
|
|
5
|
+
readonly statusCode: number;
|
|
6
|
+
readonly errorMessage: string;
|
|
7
|
+
constructor(statusCode: number, errorMessage: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class GislValidationError extends GislApiError {
|
|
10
|
+
readonly details: Array<{
|
|
11
|
+
field: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
constructor(statusCode: number, errorMessage: string, details: Array<{
|
|
15
|
+
field: string;
|
|
16
|
+
message: string;
|
|
17
|
+
}>);
|
|
18
|
+
}
|
|
19
|
+
export declare class GislTimeoutError extends GislError {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class GislError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'GislError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class GislApiError extends GislError {
|
|
8
|
+
statusCode;
|
|
9
|
+
errorMessage;
|
|
10
|
+
constructor(statusCode, errorMessage) {
|
|
11
|
+
super(`API error ${statusCode}: ${errorMessage}`);
|
|
12
|
+
this.name = 'GislApiError';
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.errorMessage = errorMessage;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class GislValidationError extends GislApiError {
|
|
18
|
+
details;
|
|
19
|
+
constructor(statusCode, errorMessage, details) {
|
|
20
|
+
super(statusCode, errorMessage);
|
|
21
|
+
this.name = 'GislValidationError';
|
|
22
|
+
this.details = details;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class GislTimeoutError extends GislError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'GislTimeoutError';
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { GislClient } from './client.js';
|
|
2
|
+
export { verifyWebhook } from './webhook.js';
|
|
3
|
+
export { parseSseStream } from './sse.js';
|
|
4
|
+
export type { GislClientConfig, GislSseEvent, UploadOptions, WaitOptions, WorkflowCreatePayload, OperationDef, FileJobPayload, SourceJobPayload, InputsJobPayload, JobDefinitionPayload, } from './types.js';
|
|
5
|
+
export { fileJob, sourceJob, inputsJob } from './types.js';
|
|
6
|
+
export { GislError, GislApiError, GislValidationError, GislTimeoutError, } from './errors.js';
|
|
7
|
+
export type { UploadResponse, WorkflowCreateResponse, WorkflowStatusResponse, WorkflowDownloadResponse, MetadataResponse, OperationsSchemaResponse, RetryResponse, JobDownload, OperationDownload, WebhookPayload, JobResponse, OperationResponse, OperationResult, ExportConfig, } from '@giveitsmaller/contracts/openapi';
|
|
8
|
+
export { WorkflowStatus, OperationType, SseEventType, CallbackEventType, OperationStatus, JobStatus, } from '@giveitsmaller/contracts/openapi';
|
|
9
|
+
export type { SseOperationProgressData, SseOperationCompletedData, SseOperationFailedData, SseJobCompletedData, SseJobFailedData, SseWorkflowTerminalData, } from '@giveitsmaller/contracts/openapi';
|
|
10
|
+
export type { CompressImageOptions, CompressVideoOptions, CompressAudioOptions, CompressDocumentPdfOptions, CompressDocumentOfficeOptions, CompressDocumentOdfOptions, CompressDocumentEpubOptions, ThumbnailImageOptions, ThumbnailVideoOptions, ThumbnailDocumentOptions, ConvertImageOptions, ConvertVideoOptions, ConvertAudioOptions, ConvertDocumentPdfOptions, MergeImageOptions, MergeVideoOptions, MergeAudioOptions, MergeDocumentOptions, ArchiveOptions, } from '@giveitsmaller/contracts/operations';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SDK classes and functions
|
|
2
|
+
export { GislClient } from './client.js';
|
|
3
|
+
export { verifyWebhook } from './webhook.js';
|
|
4
|
+
export { parseSseStream } from './sse.js';
|
|
5
|
+
export { fileJob, sourceJob, inputsJob } from './types.js';
|
|
6
|
+
// Errors
|
|
7
|
+
export { GislError, GislApiError, GislValidationError, GislTimeoutError, } from './errors.js';
|
|
8
|
+
export { WorkflowStatus, OperationType, SseEventType, CallbackEventType, OperationStatus, JobStatus, } from '@giveitsmaller/contracts/openapi';
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GislSseEvent } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse an SSE stream from a fetch Response into an AsyncIterable of typed events.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Chunk boundary buffering (events split across chunks)
|
|
7
|
+
* - Multi-line `data:` fields (concatenated with newlines)
|
|
8
|
+
* - Comment lines (`:` prefix) used as keep-alives
|
|
9
|
+
* - `retry:` field (ignored, SDK manages its own reconnection)
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseSseStream(response: Response): AsyncGenerator<GislSseEvent>;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an SSE stream from a fetch Response into an AsyncIterable of typed events.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Chunk boundary buffering (events split across chunks)
|
|
6
|
+
* - Multi-line `data:` fields (concatenated with newlines)
|
|
7
|
+
* - Comment lines (`:` prefix) used as keep-alives
|
|
8
|
+
* - `retry:` field (ignored, SDK manages its own reconnection)
|
|
9
|
+
*/
|
|
10
|
+
export async function* parseSseStream(response) {
|
|
11
|
+
const body = response.body;
|
|
12
|
+
if (!body) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const reader = body.getReader();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
let buffer = '';
|
|
18
|
+
let eventType = '';
|
|
19
|
+
let dataLines = [];
|
|
20
|
+
try {
|
|
21
|
+
while (true) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done)
|
|
24
|
+
break;
|
|
25
|
+
buffer += decoder.decode(value, { stream: true });
|
|
26
|
+
// SSE spec allows \n, \r\n, and \r as line endings
|
|
27
|
+
const lines = buffer.replace(/\r\n?/g, '\n').split('\n');
|
|
28
|
+
// Keep the last (potentially incomplete) line in the buffer
|
|
29
|
+
buffer = lines.pop() ?? '';
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line === '') {
|
|
32
|
+
// Empty line = end of event
|
|
33
|
+
if (dataLines.length > 0) {
|
|
34
|
+
const rawData = dataLines.join('\n');
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(rawData);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
parsed = rawData;
|
|
41
|
+
}
|
|
42
|
+
yield {
|
|
43
|
+
event: eventType || 'message',
|
|
44
|
+
data: parsed,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
eventType = '';
|
|
48
|
+
dataLines = [];
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith(':')) {
|
|
52
|
+
// Comment line (keep-alive), skip
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const colonIndex = line.indexOf(':');
|
|
56
|
+
if (colonIndex === -1) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const field = line.slice(0, colonIndex);
|
|
60
|
+
// Strip single leading space after colon per SSE spec
|
|
61
|
+
const valueStart = line[colonIndex + 1] === ' ' ? colonIndex + 2 : colonIndex + 1;
|
|
62
|
+
const fieldValue = line.slice(valueStart);
|
|
63
|
+
switch (field) {
|
|
64
|
+
case 'event':
|
|
65
|
+
eventType = fieldValue;
|
|
66
|
+
break;
|
|
67
|
+
case 'data':
|
|
68
|
+
dataLines.push(fieldValue);
|
|
69
|
+
break;
|
|
70
|
+
case 'retry':
|
|
71
|
+
// Ignored — SDK manages its own polling/reconnection
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Flush any remaining buffered event
|
|
77
|
+
if (dataLines.length > 0) {
|
|
78
|
+
const rawData = dataLines.join('\n');
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(rawData);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
parsed = rawData;
|
|
85
|
+
}
|
|
86
|
+
yield {
|
|
87
|
+
event: eventType || 'message',
|
|
88
|
+
data: parsed,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
reader.releaseLock();
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { OperationType, CallbackEventType, SseEventType, SseOperationProgressData, SseOperationCompletedData, SseOperationFailedData, SseJobCompletedData, SseJobFailedData, SseWorkflowTerminalData } from '@giveitsmaller/contracts/openapi';
|
|
2
|
+
export interface GislClientConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
/** Threshold in bytes above which multipart upload is used (default: 10MB) */
|
|
8
|
+
multipartThreshold?: number;
|
|
9
|
+
/** Max concurrent chunk uploads for multipart (default: 4) */
|
|
10
|
+
multipartConcurrency?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface OperationDef {
|
|
13
|
+
type: OperationType;
|
|
14
|
+
options?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
/** Job sourced from an uploaded file */
|
|
17
|
+
export interface FileJobPayload {
|
|
18
|
+
ref: string;
|
|
19
|
+
file_id: string;
|
|
20
|
+
operations: OperationDef[];
|
|
21
|
+
}
|
|
22
|
+
/** Job sourced from a single upstream job's output */
|
|
23
|
+
export interface SourceJobPayload {
|
|
24
|
+
ref: string;
|
|
25
|
+
source: {
|
|
26
|
+
ref: string;
|
|
27
|
+
operation?: string;
|
|
28
|
+
};
|
|
29
|
+
operations: OperationDef[];
|
|
30
|
+
}
|
|
31
|
+
/** Job sourced from multiple upstream jobs (merge/archive) */
|
|
32
|
+
export interface InputsJobPayload {
|
|
33
|
+
ref: string;
|
|
34
|
+
inputs: Array<{
|
|
35
|
+
ref: string;
|
|
36
|
+
operation?: string;
|
|
37
|
+
per_input_options?: Record<string, unknown>;
|
|
38
|
+
}>;
|
|
39
|
+
operations: OperationDef[];
|
|
40
|
+
}
|
|
41
|
+
export type JobDefinitionPayload = FileJobPayload | SourceJobPayload | InputsJobPayload;
|
|
42
|
+
export declare function fileJob(ref: string, fileId: string, operations: OperationDef[]): FileJobPayload;
|
|
43
|
+
export declare function sourceJob(ref: string, source: {
|
|
44
|
+
ref: string;
|
|
45
|
+
operation?: string;
|
|
46
|
+
}, operations: OperationDef[]): SourceJobPayload;
|
|
47
|
+
export declare function inputsJob(ref: string, inputs: Array<{
|
|
48
|
+
ref: string;
|
|
49
|
+
operation?: string;
|
|
50
|
+
per_input_options?: Record<string, unknown>;
|
|
51
|
+
}>, operations: OperationDef[]): InputsJobPayload;
|
|
52
|
+
export interface WorkflowCreatePayload {
|
|
53
|
+
jobs: JobDefinitionPayload[];
|
|
54
|
+
workflow_edges?: Array<{
|
|
55
|
+
from: string;
|
|
56
|
+
to: string;
|
|
57
|
+
}>;
|
|
58
|
+
callback_url?: string;
|
|
59
|
+
callback_events?: CallbackEventType[];
|
|
60
|
+
export?: {
|
|
61
|
+
service: 's3';
|
|
62
|
+
bucket: string;
|
|
63
|
+
key_prefix?: string;
|
|
64
|
+
role_arn: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface WaitOptions {
|
|
68
|
+
/** Poll interval in milliseconds (default: 2000) */
|
|
69
|
+
intervalMs?: number;
|
|
70
|
+
/** Maximum wait time in milliseconds (default: 300000 = 5 min) */
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
/** Called after each poll with current status */
|
|
73
|
+
onPoll?: (status: string) => void;
|
|
74
|
+
}
|
|
75
|
+
export type GislSseEvent = {
|
|
76
|
+
event: typeof SseEventType.operation_progress;
|
|
77
|
+
data: SseOperationProgressData;
|
|
78
|
+
} | {
|
|
79
|
+
event: typeof SseEventType.operation_completed;
|
|
80
|
+
data: SseOperationCompletedData;
|
|
81
|
+
} | {
|
|
82
|
+
event: typeof SseEventType.operation_failed;
|
|
83
|
+
data: SseOperationFailedData;
|
|
84
|
+
} | {
|
|
85
|
+
event: typeof SseEventType.job_completed;
|
|
86
|
+
data: SseJobCompletedData;
|
|
87
|
+
} | {
|
|
88
|
+
event: typeof SseEventType.job_failed;
|
|
89
|
+
data: SseJobFailedData;
|
|
90
|
+
} | {
|
|
91
|
+
event: typeof SseEventType.workflow_completed;
|
|
92
|
+
data: SseWorkflowTerminalData;
|
|
93
|
+
} | {
|
|
94
|
+
event: typeof SseEventType.workflow_failed;
|
|
95
|
+
data: SseWorkflowTerminalData;
|
|
96
|
+
} | {
|
|
97
|
+
event: typeof SseEventType.workflow_partially_failed;
|
|
98
|
+
data: SseWorkflowTerminalData;
|
|
99
|
+
} | {
|
|
100
|
+
event: string;
|
|
101
|
+
data: unknown;
|
|
102
|
+
};
|
|
103
|
+
export interface UploadOptions {
|
|
104
|
+
/** Called with bytes uploaded so far (only for multipart) */
|
|
105
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void;
|
|
106
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Job factory functions
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export function fileJob(ref, fileId, operations) {
|
|
5
|
+
return { ref, file_id: fileId, operations };
|
|
6
|
+
}
|
|
7
|
+
export function sourceJob(ref, source, operations) {
|
|
8
|
+
return { ref, source, operations };
|
|
9
|
+
}
|
|
10
|
+
export function inputsJob(ref, inputs, operations) {
|
|
11
|
+
return { ref, inputs, operations };
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a GISL webhook signature.
|
|
3
|
+
*
|
|
4
|
+
* @param secret The `webhook_secret` from the workflow creation response.
|
|
5
|
+
* @param signature The value of the `X-GIS-Signature` header.
|
|
6
|
+
* @param body The raw request body as a string or Buffer.
|
|
7
|
+
* @returns `true` if valid.
|
|
8
|
+
* @throws {GislError} if the signature is missing, malformed, or invalid.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyWebhook(secret: string, signature: string, body: string | Buffer): boolean;
|