flexorch-sdk 0.2.2 → 0.3.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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dataset
3
- } from "./chunk-35RGZSFP.js";
3
+ } from "./chunk-4KCMMRD7.js";
4
4
 
5
5
  // src/errors.ts
6
6
  var FlexOrchError = class extends Error {
@@ -104,6 +104,12 @@ async function parseError(res) {
104
104
  function sleep(ms) {
105
105
  return new Promise((r) => setTimeout(r, ms));
106
106
  }
107
+ function unwrap(body) {
108
+ if (body !== null && typeof body === "object" && "data" in body && "error" in body && typeof body["status"] === "string") {
109
+ return body["data"];
110
+ }
111
+ return body;
112
+ }
107
113
  var Transport = class {
108
114
  baseUrl;
109
115
  defaultHeaders;
@@ -117,7 +123,7 @@ var Transport = class {
117
123
  this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
118
124
  this.defaultHeaders = {
119
125
  "X-API-KEY": apiKey,
120
- "User-Agent": "flexorch-sdk-js/0.1.0"
126
+ "User-Agent": "flexorch-sdk-js/0.3.0"
121
127
  };
122
128
  }
123
129
  url(path, params) {
@@ -158,7 +164,7 @@ var Transport = class {
158
164
  if (!ct.includes("application/json")) return null;
159
165
  const text = await res.text();
160
166
  if (!text.trim()) return null;
161
- return JSON.parse(text);
167
+ return unwrap(JSON.parse(text));
162
168
  } catch (err) {
163
169
  clearTimeout(timer);
164
170
  if (err instanceof FlexOrchError) throw err;
@@ -191,13 +197,35 @@ var Transport = class {
191
197
  };
192
198
 
193
199
  // src/models/job.ts
200
+ function jobFeedbackFromDict(data) {
201
+ return {
202
+ id: String(data["id"] ?? ""),
203
+ jobId: String(data["job_id"] ?? ""),
204
+ rating: String(data["rating"] ?? ""),
205
+ issue: data["issue"] ?? null,
206
+ notes: data["notes"] ?? null,
207
+ createdAt: String(data["created_at"] ?? "")
208
+ };
209
+ }
194
210
  var Job = class _Job {
195
211
  id;
196
212
  status;
197
213
  qualityGrade;
198
214
  qualityScore;
199
215
  documentId;
216
+ /** Needed by buildDataset() — POST /datasets/build-from-execution/{executionId}. */
217
+ executionId;
200
218
  hasDataset;
219
+ /**
220
+ * True when the underlying pipeline execution completed but one or more
221
+ * non-critical steps failed (e.g. structured extraction couldn't find a
222
+ * table in the document). The job still succeeds — PII detection and
223
+ * quality scoring results are still meaningful — but `records`/columns
224
+ * may be empty. Read from `execution_summary.degraded`; false for jobs
225
+ * with no execution (e.g. dataset_build). wait() does not throw for a
226
+ * degraded completion.
227
+ */
228
+ degraded;
201
229
  failureReason;
202
230
  _transport;
203
231
  constructor(data) {
@@ -206,19 +234,30 @@ var Job = class _Job {
206
234
  this.qualityGrade = data.qualityGrade;
207
235
  this.qualityScore = data.qualityScore;
208
236
  this.documentId = data.documentId;
237
+ this.executionId = data.executionId;
209
238
  this.hasDataset = data.hasDataset;
239
+ this.degraded = data.degraded;
210
240
  this.failureReason = data.failureReason;
211
241
  this._transport = data._transport;
212
242
  }
213
243
  static fromDict(data, transport) {
214
- const quality = data["quality"] ?? {};
244
+ const executionSummary = data["execution_summary"];
245
+ const processingSummary = data["processing_summary"];
246
+ let quality = data["quality"];
247
+ if (!quality && processingSummary) {
248
+ quality = processingSummary["quality"];
249
+ }
250
+ quality = quality ?? {};
251
+ const executionId = executionSummary?.["execution_id"] ?? processingSummary?.["execution_id"] ?? data["execution_id"] ?? null;
215
252
  return new _Job({
216
253
  id: String(data["job_id"] ?? data["id"] ?? ""),
217
254
  status: String(data["status"] ?? ""),
218
255
  qualityGrade: quality["grade"] ?? null,
219
256
  qualityScore: quality["score"] ?? null,
220
257
  documentId: data["document_id"] ?? null,
221
- hasDataset: Boolean(data["has_dataset"] ?? false),
258
+ executionId,
259
+ hasDataset: Boolean(data["has_dataset"] ?? processingSummary?.["has_dataset"] ?? false),
260
+ degraded: Boolean(executionSummary?.["degraded"] ?? false),
222
261
  failureReason: data["failure_reason"] ?? null,
223
262
  _transport: transport
224
263
  });
@@ -251,9 +290,46 @@ var Job = class _Job {
251
290
  });
252
291
  const items = data["items"] ?? [];
253
292
  if (items.length === 0) return null;
254
- const { Dataset: Dataset2 } = await import("./dataset-PP755LIW.js");
293
+ const { Dataset: Dataset2 } = await import("./dataset-E7EXJETI.js");
255
294
  return Dataset2.fromDict(items[0], this._transport);
256
295
  }
296
+ /**
297
+ * Build a dataset from this job's execution.
298
+ *
299
+ * A completed data_process job does not have a dataset yet — building one
300
+ * is a separate, explicit step (`POST
301
+ * /datasets/build-from-execution/{executionId}`). Call this after
302
+ * `.wait()`, then `.wait()` again on the returned dataset_build Job before
303
+ * calling `.dataset()`:
304
+ *
305
+ * ```ts
306
+ * const job = await client.process("invoice.pdf");
307
+ * const done = await job.wait();
308
+ * const dataset = await (await done.buildDataset()).wait().then(j => j.dataset());
309
+ * ```
310
+ *
311
+ * @throws {Error} If this job has no executionId to build a dataset from
312
+ * (e.g. it failed, or is itself a dataset_build job).
313
+ */
314
+ async buildDataset(opts = {}) {
315
+ if (!this.executionId) {
316
+ throw new Error(
317
+ `Job ${this.id} has no executionId to build a dataset from (job must be a completed data_process job).`
318
+ );
319
+ }
320
+ const body = {
321
+ force_rebuild: opts.forceRebuild ?? false,
322
+ replace_existing: opts.replaceExisting ?? false
323
+ };
324
+ if (opts.name !== void 0) body["name"] = opts.name;
325
+ if (opts.description !== void 0) body["description"] = opts.description;
326
+ if (opts.slug !== void 0) body["slug"] = opts.slug;
327
+ const data = await this._transport.post(
328
+ `/datasets/build-from-execution/${this.executionId}`,
329
+ body
330
+ );
331
+ return _Job.fromDict(data, this._transport);
332
+ }
257
333
  toString() {
258
334
  return `Job(id=${this.id}, status=${this.status}, grade=${this.qualityGrade})`;
259
335
  }
@@ -297,6 +373,15 @@ var SearchResult = class _SearchResult {
297
373
  };
298
374
 
299
375
  // src/resources/jobs.ts
376
+ var VALID_RATINGS = /* @__PURE__ */ new Set(["up", "down"]);
377
+ var VALID_ISSUES = /* @__PURE__ */ new Set([
378
+ "wrong_doc_type",
379
+ "missing_fields",
380
+ "wrong_values",
381
+ "pii_missed",
382
+ "pii_over_masked",
383
+ "other"
384
+ ]);
300
385
  var JobsResource = class {
301
386
  constructor(_t) {
302
387
  this._t = _t;
@@ -314,6 +399,34 @@ var JobsResource = class {
314
399
  const items = data["items"] ?? [];
315
400
  return items.map((item) => Job.fromDict(item, this._t));
316
401
  }
402
+ /**
403
+ * Submit user feedback for a completed job. Upsert — a second call for the
404
+ * same job replaces the previous feedback.
405
+ *
406
+ * @param rating "up" or "down".
407
+ * @param opts.issue When rating="down": "wrong_doc_type" | "missing_fields" |
408
+ * "wrong_values" | "pii_missed" | "pii_over_masked" | "other".
409
+ */
410
+ async submitFeedback(jobId, rating, opts = {}) {
411
+ if (!VALID_RATINGS.has(rating)) {
412
+ throw new Error(`Invalid rating "${rating}". Valid: ${[...VALID_RATINGS].join(", ")}`);
413
+ }
414
+ if (opts.issue !== void 0 && !VALID_ISSUES.has(opts.issue)) {
415
+ throw new Error(`Invalid issue "${opts.issue}". Valid: ${[...VALID_ISSUES].join(", ")}`);
416
+ }
417
+ const data = await this._t.post(`/jobs/${jobId}/feedback`, {
418
+ rating,
419
+ issue: opts.issue ?? null,
420
+ notes: opts.notes ?? null
421
+ });
422
+ return jobFeedbackFromDict(data);
423
+ }
424
+ /** Existing feedback for a job, or null if none was submitted. */
425
+ async getFeedback(jobId) {
426
+ const data = await this._t.get(`/jobs/${jobId}/feedback`);
427
+ if (!data) return null;
428
+ return jobFeedbackFromDict(data);
429
+ }
317
430
  };
318
431
 
319
432
  // src/resources/datasets.ts
@@ -330,10 +443,121 @@ var DatasetsResource = class {
330
443
  const params = {};
331
444
  if (opts.page !== void 0) params["page"] = String(opts.page);
332
445
  if (opts.pageSize !== void 0) params["page_size"] = String(opts.pageSize);
446
+ if (opts.status !== void 0) params["status"] = opts.status;
447
+ if (opts.sourceExecutionId !== void 0) params["source_execution_id"] = String(opts.sourceExecutionId);
448
+ if (opts.sourceDocumentId !== void 0) params["source_document_id"] = String(opts.sourceDocumentId);
449
+ if (opts.q !== void 0) params["q"] = opts.q;
333
450
  const data = await this._t.get("/datasets", params);
334
451
  const items = data["items"] ?? [];
335
452
  return items.map((item) => Dataset.fromDict(item, this._t));
336
453
  }
454
+ /**
455
+ * Build a dataset from a completed execution.
456
+ *
457
+ * Prefer `Job.buildDataset()` when you already have a Job object — this is
458
+ * the lower-level call for when you only have an executionId (e.g. from
459
+ * `Document.latestExecution`).
460
+ *
461
+ * @returns A dataset_build Job — call `.wait()` then `.dataset()`.
462
+ */
463
+ async buildFromExecution(executionId, opts = {}) {
464
+ const body = {
465
+ force_rebuild: opts.forceRebuild ?? false,
466
+ replace_existing: opts.replaceExisting ?? false
467
+ };
468
+ if (opts.name !== void 0) body["name"] = opts.name;
469
+ if (opts.description !== void 0) body["description"] = opts.description;
470
+ if (opts.slug !== void 0) body["slug"] = opts.slug;
471
+ const data = await this._t.post(
472
+ `/datasets/build-from-execution/${executionId}`,
473
+ body
474
+ );
475
+ return Job.fromDict(data, this._t);
476
+ }
477
+ };
478
+
479
+ // src/models/document.ts
480
+ var Document = class _Document {
481
+ id;
482
+ filename;
483
+ fileExt;
484
+ status;
485
+ storagePath;
486
+ createdAt;
487
+ processingCount;
488
+ latestExecution;
489
+ dataset;
490
+ processingHistory;
491
+ relatedDatasets;
492
+ _transport;
493
+ constructor(data) {
494
+ this.id = data.id;
495
+ this.filename = data.filename;
496
+ this.fileExt = data.fileExt;
497
+ this.status = data.status;
498
+ this.storagePath = data.storagePath;
499
+ this.createdAt = data.createdAt;
500
+ this.processingCount = data.processingCount;
501
+ this.latestExecution = data.latestExecution;
502
+ this.dataset = data.dataset;
503
+ this.processingHistory = data.processingHistory;
504
+ this.relatedDatasets = data.relatedDatasets;
505
+ this._transport = data._transport;
506
+ }
507
+ static fromDict(data, transport) {
508
+ return new _Document({
509
+ id: String(data["id"] ?? ""),
510
+ filename: String(data["filename"] ?? ""),
511
+ fileExt: String(data["file_ext"] ?? ""),
512
+ status: String(data["status"] ?? ""),
513
+ storagePath: String(data["storage_path"] ?? ""),
514
+ createdAt: String(data["created_at"] ?? ""),
515
+ processingCount: Number(data["processing_count"] ?? 0),
516
+ latestExecution: data["latest_execution"] ?? null,
517
+ dataset: data["dataset"] ?? null,
518
+ processingHistory: data["processing_history"] ?? [],
519
+ relatedDatasets: data["related_datasets"] ?? [],
520
+ _transport: transport
521
+ });
522
+ }
523
+ /**
524
+ * Re-queue this document through the processing pipeline.
525
+ *
526
+ * Raises (via the API): 400 DOCUMENT_FILE_NOT_AVAILABLE if the source file
527
+ * is no longer on disk, or 400 REPROCESS_NOT_SUPPORTED for
528
+ * connector-sourced (e.g. S3) documents.
529
+ */
530
+ async reprocess(pipelineConfig) {
531
+ const body = {};
532
+ if (pipelineConfig) body["pipeline_config"] = pipelineConfig;
533
+ const data = await this._transport.post(`/documents/${this.id}/reprocess`, body);
534
+ return Job.fromDict(data, this._transport);
535
+ }
536
+ toString() {
537
+ return `Document(id=${this.id}, filename=${this.filename}, status=${this.status})`;
538
+ }
539
+ };
540
+
541
+ // src/resources/documents.ts
542
+ var DocumentsResource = class {
543
+ constructor(_t) {
544
+ this._t = _t;
545
+ }
546
+ _t;
547
+ /** Fetch a single document, including processingHistory and relatedDatasets. */
548
+ async get(documentId) {
549
+ const data = await this._t.get(`/documents/${documentId}`);
550
+ return Document.fromDict(data, this._t);
551
+ }
552
+ /** List documents for the current tenant, newest first. */
553
+ async list(opts = {}) {
554
+ const params = {};
555
+ if (opts.page !== void 0) params["page"] = String(opts.page);
556
+ if (opts.pageSize !== void 0) params["page_size"] = String(opts.pageSize);
557
+ const data = await this._t.get("/documents", params);
558
+ const items = data["items"] ?? [];
559
+ return items.map((item) => Document.fromDict(item, this._t));
560
+ }
337
561
  };
338
562
 
339
563
  // src/resources/usage.ts
@@ -343,15 +567,51 @@ var UsageResource = class {
343
567
  }
344
568
  _t;
345
569
  async current() {
346
- const data = await this._t.get("/usage/current");
570
+ const data = await this._t.get("/usage") ?? {};
571
+ const trial = data["trial"] ?? {};
572
+ const usage = data["usage"] ?? {};
573
+ const credits = usage["credits"] ?? {};
347
574
  return {
348
575
  plan: String(data["plan"] ?? ""),
349
- creditsUsed: Number(data["credits_used"] ?? 0),
350
- creditsLimit: Number(data["credits_limit"] ?? 0),
351
- creditsRemaining: Number(data["credits_remaining"] ?? 0),
352
- resetAt: String(data["reset_at"] ?? ""),
353
- periodStart: String(data["period_start"] ?? ""),
354
- periodEnd: String(data["period_end"] ?? "")
576
+ creditsUsed: Number(credits["used"] ?? 0),
577
+ creditsLimit: credits["limit"] ?? null,
578
+ creditsRemaining: credits["remaining"] ?? null,
579
+ isTrial: Boolean(trial["is_trial"] ?? false),
580
+ trialEndsAt: trial["trial_ends_at"] ?? null,
581
+ trialDaysRemaining: trial["trial_days_remaining"] ?? null
582
+ };
583
+ }
584
+ /** @param period "7d" | "30d" | "90d". Default: "30d". */
585
+ async history(period = "30d") {
586
+ const data = await this._t.get("/usage/history", { period });
587
+ return (data ?? []).map((item) => ({
588
+ date: String(item["date"] ?? ""),
589
+ creditsUsed: Number(item["credits_used"] ?? 0),
590
+ jobsCount: Number(item["jobs_count"] ?? 0)
591
+ }));
592
+ }
593
+ /** @param period "7d" | "30d" | "90d". Default: "30d". */
594
+ async qualityTrend(period = "30d") {
595
+ const data = await this._t.get("/usage/quality-trend", { period });
596
+ return (data ?? []).map((item) => ({
597
+ date: String(item["date"] ?? ""),
598
+ avgQualityScore: Number(item["avg_quality_score"] ?? 0),
599
+ gradeDistribution: item["grade_distribution"] ?? {},
600
+ avgFieldFillRate: item["avg_field_fill_rate"] ?? null,
601
+ jobCount: Number(item["job_count"] ?? 0)
602
+ }));
603
+ }
604
+ /** Current rate limit configuration and window usage. Does not consume a request slot. */
605
+ async rateLimits() {
606
+ const data = await this._t.get("/usage/rate-limits") ?? {};
607
+ return {
608
+ plan: String(data["plan"] ?? ""),
609
+ unlimited: Boolean(data["unlimited"] ?? false),
610
+ limit: data["limit"] ?? null,
611
+ used: data["used"] ?? null,
612
+ remaining: data["remaining"] ?? null,
613
+ windowSeconds: Number(data["window_seconds"] ?? 0),
614
+ resetInSeconds: data["reset_in_seconds"] ?? null
355
615
  };
356
616
  }
357
617
  };
@@ -423,6 +683,32 @@ var Connector = class _Connector {
423
683
  return `Connector(id=${this.id}, name=${this.name}, type=${this.type})`;
424
684
  }
425
685
  };
686
+ function syncScheduleFromDict(data) {
687
+ return {
688
+ id: String(data["id"] ?? ""),
689
+ connectorId: String(data["connector_id"] ?? ""),
690
+ cronExpression: String(data["cron_expression"] ?? ""),
691
+ prefixFilter: data["prefix_filter"] ?? null,
692
+ isActive: Boolean(data["is_active"] ?? true),
693
+ lastRunAt: data["last_run_at"] ?? null,
694
+ nextRunAt: data["next_run_at"] ?? null,
695
+ createdAt: String(data["created_at"] ?? "")
696
+ };
697
+ }
698
+ function syncLogFromDict(data) {
699
+ return {
700
+ id: String(data["id"] ?? ""),
701
+ scheduleId: String(data["schedule_id"] ?? ""),
702
+ startedAt: String(data["started_at"] ?? ""),
703
+ completedAt: data["completed_at"] ?? null,
704
+ filesFound: Number(data["files_found"] ?? 0),
705
+ filesNew: Number(data["files_new"] ?? 0),
706
+ filesSkipped: Number(data["files_skipped"] ?? 0),
707
+ filesFailed: Number(data["files_failed"] ?? 0),
708
+ status: String(data["status"] ?? ""),
709
+ errorMessage: data["error_message"] ?? null
710
+ };
711
+ }
426
712
 
427
713
  // src/resources/connectors.ts
428
714
  var VALID_TYPES = /* @__PURE__ */ new Set([
@@ -466,13 +752,54 @@ var ConnectorsResource = class {
466
752
  message: String(data["message"] ?? "")
467
753
  };
468
754
  }
755
+ /** Define a scheduled sync for a connector (Pro+ required). */
756
+ async createSchedule(connectorId, cronExpression, prefixFilter = null) {
757
+ const data = await this._t.post(`/connectors/${connectorId}/schedules`, {
758
+ cron_expression: cronExpression,
759
+ prefix_filter: prefixFilter
760
+ });
761
+ return syncScheduleFromDict(data);
762
+ }
763
+ /** Active schedules for a connector. */
764
+ async listSchedules(connectorId) {
765
+ const data = await this._t.get(`/connectors/${connectorId}/schedules`);
766
+ return (data ?? []).map(syncScheduleFromDict);
767
+ }
768
+ /** Delete a scheduled sync. */
769
+ async deleteSchedule(connectorId, scheduleId) {
770
+ await this._t.delete(`/connectors/${connectorId}/schedules/${scheduleId}`);
771
+ }
772
+ /** Run a schedule immediately instead of waiting for its cron time. */
773
+ async triggerSchedule(connectorId, scheduleId) {
774
+ const data = await this._t.post(
775
+ `/connectors/${connectorId}/schedules/${scheduleId}/trigger`
776
+ );
777
+ return syncLogFromDict(data);
778
+ }
779
+ /** Recent sync run logs for a schedule. */
780
+ async scheduleLogs(connectorId, scheduleId) {
781
+ const data = await this._t.get(
782
+ `/connectors/${connectorId}/schedules/${scheduleId}/logs`
783
+ );
784
+ return (data ?? []).map(syncLogFromDict);
785
+ }
469
786
  };
470
787
 
471
788
  // src/client.ts
472
789
  var DEFAULT_BASE_URL = "https://api.flexorch.com/v1";
790
+ function firstJobOrThrow(uploadResponse, filename, transport) {
791
+ const jobs = uploadResponse["jobs"] ?? [];
792
+ if (jobs.length > 0) {
793
+ return Job.fromDict(jobs[0], transport);
794
+ }
795
+ const rejected = uploadResponse["rejected"] ?? [];
796
+ const reason = rejected.length > 0 ? String(rejected[0]["error"]) : "unknown error";
797
+ throw new ValidationError(`${filename} was rejected: ${reason}`);
798
+ }
473
799
  var FlexOrchClient = class {
474
800
  jobs;
475
801
  datasets;
802
+ documents;
476
803
  usage;
477
804
  webhooks;
478
805
  connectors;
@@ -494,6 +821,7 @@ var FlexOrchClient = class {
494
821
  );
495
822
  this.jobs = new JobsResource(this._transport);
496
823
  this.datasets = new DatasetsResource(this._transport);
824
+ this.documents = new DocumentsResource(this._transport);
497
825
  this.usage = new UsageResource(this._transport);
498
826
  this.webhooks = new WebhooksResource(this._transport);
499
827
  this.connectors = new ConnectorsResource(this._transport);
@@ -511,13 +839,13 @@ var FlexOrchClient = class {
511
839
  chunks.push(chunk);
512
840
  }
513
841
  const blob = new Blob([Buffer.concat(chunks)], { type: "application/octet-stream" });
514
- form.append("file", blob, basename(filePath));
842
+ form.append("files", blob, basename(filePath));
515
843
  form.append("locale", opts.locale ?? "und");
516
844
  if (opts.pipelineConfig) {
517
845
  form.append("pipeline_config", JSON.stringify(opts.pipelineConfig));
518
846
  }
519
847
  const data = await this._transport.postForm("/data-process/async", form);
520
- return Job.fromDict(data, this._transport);
848
+ return firstJobOrThrow(data, basename(filePath), this._transport);
521
849
  }
522
850
  async processMany(filePaths, opts = {}) {
523
851
  const jobs = [];
@@ -536,7 +864,7 @@ var FlexOrchClient = class {
536
864
  form.append("pipeline_config", JSON.stringify(opts.pipelineConfig));
537
865
  }
538
866
  const data = await this._transport.postForm("/data-process/async", form);
539
- jobs.push(Job.fromDict(data, this._transport));
867
+ jobs.push(firstJobOrThrow(data, key, this._transport));
540
868
  }
541
869
  return jobs;
542
870
  }
@@ -740,11 +1068,12 @@ var FlexOrchReader = class {
740
1068
  };
741
1069
 
742
1070
  // src/index.ts
743
- var version = "0.2.0";
1071
+ var version = "0.3.0";
744
1072
  export {
745
1073
  AuthError,
746
1074
  Connector,
747
1075
  Dataset,
1076
+ Document,
748
1077
  FlexOrchClient,
749
1078
  FlexOrchError,
750
1079
  FlexOrchReader,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexorch-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript/JavaScript SDK for the FlexOrch API — process documents, build LLM-ready datasets",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",