diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md index c08e9b6..9568e69 100644 --- a/ATTRIBUTIONS.md +++ b/ATTRIBUTIONS.md @@ -24468,7 +24468,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ## lodash-es -**Version:** 4.17.21 +**Version:** 4.17.23 **License:** MIT ``` diff --git a/packages/sdk/src/entities/application-run/process-application-run.spec.ts b/packages/sdk/src/entities/application-run/process-application-run.spec.ts new file mode 100644 index 0000000..a8692ec --- /dev/null +++ b/packages/sdk/src/entities/application-run/process-application-run.spec.ts @@ -0,0 +1,123 @@ +import { describe, it, expect } from 'vitest'; +import { processApplicationRun } from './process-application-run.js'; +import type { RunReadResponse } from '../../generated/index.js'; + +function buildRun( + overrides: Partial> & { + statistics?: Partial; + } = {} +): RunReadResponse { + return { + run_id: 'run-1', + application_id: 'app-1', + version_number: '1.0.0', + state: overrides.state ?? 'PENDING', + output: 'NONE', + termination_reason: overrides.termination_reason ?? null, + error_code: null, + error_message: null, + submitted_at: '2026-01-01T00:00:00Z', + submitted_by: 'user-1', + statistics: { + item_count: 0, + item_pending_count: 0, + item_processing_count: 0, + item_user_error_count: 0, + item_system_error_count: 0, + item_skipped_count: 0, + item_succeeded_count: 0, + ...overrides.statistics, + }, + } as RunReadResponse; +} + +describe('processApplicationRun', () => { + it('should preserve all original RunReadResponse fields', () => { + const raw = buildRun({ state: 'PENDING' }); + const result = processApplicationRun(raw); + + expect(result.run_id).toBe(raw.run_id); + expect(result.application_id).toBe(raw.application_id); + expect(result.version_number).toBe(raw.version_number); + expect(result.state).toBe(raw.state); + expect(result.statistics).toEqual(raw.statistics); + }); + + it('should add progress property for a PENDING run with no items', () => { + const result = processApplicationRun(buildRun({ state: 'PENDING' })); + expect(result.progress).toBe(0); + }); + + it('should add status property for a PENDING run', () => { + const result = processApplicationRun(buildRun({ state: 'PENDING' })); + expect(result.status).toBe('PENDING'); + }); + + it('should add can_download property for a PENDING run', () => { + const result = processApplicationRun(buildRun({ state: 'PENDING' })); + expect(result.can_download).toBe(false); + }); + + it('should compute correct values for a completed run', () => { + const raw = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { item_count: 10, item_succeeded_count: 10 }, + }); + const result = processApplicationRun(raw); + + expect(result.progress).toBe(100); + expect(result.status).toBe('COMPLETED'); + expect(result.can_download).toBe(true); + }); + + it('should compute correct values for a run completed with errors', () => { + const raw = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { + item_count: 10, + item_succeeded_count: 7, + item_user_error_count: 3, + }, + }); + const result = processApplicationRun(raw); + + expect(result.progress).toBe(100); + expect(result.status).toBe('COMPLETED_WITH_ERRORS'); + expect(result.can_download).toBe(true); + }); + + it('should compute correct values for a canceled run', () => { + const raw = buildRun({ + state: 'TERMINATED', + termination_reason: 'CANCELED_BY_USER', + statistics: { + item_count: 10, + item_succeeded_count: 3, + }, + }); + const result = processApplicationRun(raw); + + expect(result.progress).toBe(30); + expect(result.status).toBe('CANCELED'); + expect(result.can_download).toBe(false); + }); + + it('should compute correct values for a processing run', () => { + const raw = buildRun({ + state: 'PROCESSING', + statistics: { + item_count: 20, + item_succeeded_count: 8, + item_processing_count: 4, + item_pending_count: 8, + }, + }); + const result = processApplicationRun(raw); + + expect(result.progress).toBe(40); + expect(result.status).toBe('PROCESSING'); + expect(result.can_download).toBe(false); + }); +}); diff --git a/packages/sdk/src/entities/application-run/process-application-run.ts b/packages/sdk/src/entities/application-run/process-application-run.ts new file mode 100644 index 0000000..7397041 --- /dev/null +++ b/packages/sdk/src/entities/application-run/process-application-run.ts @@ -0,0 +1,22 @@ +import { ApplicationRun } from './types.js'; +import { RunReadResponse } from '../../generated/index.js'; +import { getRunProgress, getRunStatus, canDownloadRunItems } from './utils.js'; + +/** + * Transform a raw {@link RunReadResponse} from the API into an enriched + * {@link ApplicationRun} by computing derived properties (progress, status, can_download). + * + * This is the single entry-point used by `PlatformSDKHttp` methods to map API + * responses before returning them to consumers. + * + * @param run - Raw run response from the API + * @returns Enriched run entity with computed properties spread onto the original response + */ +export const processApplicationRun = (run: RunReadResponse): ApplicationRun => { + return { + ...run, + progress: getRunProgress(run), + status: getRunStatus(run), + can_download: canDownloadRunItems(run), + }; +}; diff --git a/packages/sdk/src/entities/application-run/types.ts b/packages/sdk/src/entities/application-run/types.ts new file mode 100644 index 0000000..2489b88 --- /dev/null +++ b/packages/sdk/src/entities/application-run/types.ts @@ -0,0 +1,26 @@ +import { RunReadResponse } from '../../generated/index.js'; + +/** + * Enriched application run entity that extends the raw API response + * with computed properties derived from run state and statistics. + * + * Returned by `PlatformSDKHttp.listApplicationRuns()` and `PlatformSDKHttp.getRun()` + * instead of the raw `RunReadResponse`. + */ +export interface ApplicationRun extends RunReadResponse { + /** Percentage of items processed (0–100), computed from run statistics. */ + progress: number; + /** Human-readable status derived from `state` and `termination_reason`. */ + status: RunStatus; + /** Whether the run's result items are available for download. */ + can_download: boolean; +} + +/** Derived run status that simplifies the raw `state` + `termination_reason` combination. */ +export type RunStatus = + | 'PENDING' + | 'PROCESSING' + | 'COMPLETED' + | 'COMPLETED_WITH_ERRORS' + | 'CANCELED' + | 'FAILED'; diff --git a/packages/sdk/src/entities/application-run/utils.spec.ts b/packages/sdk/src/entities/application-run/utils.spec.ts new file mode 100644 index 0000000..3c9a5a6 --- /dev/null +++ b/packages/sdk/src/entities/application-run/utils.spec.ts @@ -0,0 +1,205 @@ +import { describe, it, expect } from 'vitest'; +import { getRunProgress, getRunStatus, canDownloadRunItems } from './utils.js'; +import type { RunReadResponse } from '../../generated/index.js'; + +/** + * Helper to build a minimal RunReadResponse with sensible defaults. + * Only the fields used by the utility functions need to be realistic. + */ +function buildRun( + overrides: Partial> & { + statistics?: Partial; + } = {} +): RunReadResponse { + return { + run_id: 'run-1', + application_id: 'app-1', + version_number: '1.0.0', + state: overrides.state ?? 'PENDING', + output: 'NONE', + termination_reason: overrides.termination_reason ?? null, + error_code: null, + error_message: null, + submitted_at: '2026-01-01T00:00:00Z', + submitted_by: 'user-1', + statistics: { + item_count: 0, + item_pending_count: 0, + item_processing_count: 0, + item_user_error_count: 0, + item_system_error_count: 0, + item_skipped_count: 0, + item_succeeded_count: 0, + ...overrides.statistics, + }, + } as RunReadResponse; +} + +describe('getRunProgress', () => { + it('should return 0 when item_count is 0', () => { + const run = buildRun({ statistics: { item_count: 0 } }); + expect(getRunProgress(run)).toBe(0); + }); + + it('should return 100 when all items succeeded', () => { + const run = buildRun({ + statistics: { item_count: 10, item_succeeded_count: 10 }, + }); + expect(getRunProgress(run)).toBe(100); + }); + + it('should return 50 when half the items are processed', () => { + const run = buildRun({ + statistics: { item_count: 10, item_succeeded_count: 5 }, + }); + expect(getRunProgress(run)).toBe(50); + }); + + it('should include user errors in the processed count', () => { + const run = buildRun({ + statistics: { item_count: 10, item_user_error_count: 3, item_succeeded_count: 7 }, + }); + expect(getRunProgress(run)).toBe(100); + }); + + it('should include system errors in the processed count', () => { + const run = buildRun({ + statistics: { item_count: 4, item_system_error_count: 2, item_succeeded_count: 1 }, + }); + expect(getRunProgress(run)).toBe(75); + }); + + it('should include skipped items in the processed count', () => { + const run = buildRun({ + statistics: { item_count: 5, item_skipped_count: 2, item_succeeded_count: 3 }, + }); + expect(getRunProgress(run)).toBe(100); + }); + + it('should round to the nearest integer', () => { + // 1/3 = 33.33...% → 33 + const run = buildRun({ + statistics: { item_count: 3, item_succeeded_count: 1 }, + }); + expect(getRunProgress(run)).toBe(33); + }); + + it('should combine all terminal item types', () => { + const run = buildRun({ + statistics: { + item_count: 20, + item_succeeded_count: 5, + item_user_error_count: 3, + item_system_error_count: 2, + item_skipped_count: 4, + }, + }); + // (5 + 3 + 2 + 4) / 20 = 14/20 = 70% + expect(getRunProgress(run)).toBe(70); + }); +}); + +describe('getRunStatus', () => { + it('should return PENDING when state is PENDING', () => { + const run = buildRun({ state: 'PENDING' }); + expect(getRunStatus(run)).toBe('PENDING'); + }); + + it('should return PROCESSING when state is PROCESSING', () => { + const run = buildRun({ state: 'PROCESSING' }); + expect(getRunStatus(run)).toBe('PROCESSING'); + }); + + it('should return CANCELED when terminated by user', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'CANCELED_BY_USER', + }); + expect(getRunStatus(run)).toBe('CANCELED'); + }); + + it('should return FAILED when terminated by system', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'CANCELED_BY_SYSTEM', + }); + expect(getRunStatus(run)).toBe('FAILED'); + }); + + it('should return COMPLETED when all items succeeded', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { item_count: 10, item_succeeded_count: 10 }, + }); + expect(getRunStatus(run)).toBe('COMPLETED'); + }); + + it('should return COMPLETED_WITH_ERRORS when not all items succeeded', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { item_count: 10, item_succeeded_count: 7 }, + }); + expect(getRunStatus(run)).toBe('COMPLETED_WITH_ERRORS'); + }); + + it('should return COMPLETED_WITH_ERRORS when some items had errors', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { + item_count: 10, + item_succeeded_count: 8, + item_user_error_count: 2, + }, + }); + expect(getRunStatus(run)).toBe('COMPLETED_WITH_ERRORS'); + }); +}); + +describe('canDownloadRunItems', () => { + it('should return false for PENDING runs', () => { + const run = buildRun({ state: 'PENDING' }); + expect(canDownloadRunItems(run)).toBe(false); + }); + + it('should return false for PROCESSING runs', () => { + const run = buildRun({ state: 'PROCESSING' }); + expect(canDownloadRunItems(run)).toBe(false); + }); + + it('should return false for CANCELED runs', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'CANCELED_BY_USER', + }); + expect(canDownloadRunItems(run)).toBe(false); + }); + + it('should return false for FAILED runs', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'CANCELED_BY_SYSTEM', + }); + expect(canDownloadRunItems(run)).toBe(false); + }); + + it('should return true for COMPLETED runs', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { item_count: 5, item_succeeded_count: 5 }, + }); + expect(canDownloadRunItems(run)).toBe(true); + }); + + it('should return true for COMPLETED_WITH_ERRORS runs', () => { + const run = buildRun({ + state: 'TERMINATED', + termination_reason: 'ALL_ITEMS_PROCESSED', + statistics: { item_count: 5, item_succeeded_count: 3 }, + }); + expect(canDownloadRunItems(run)).toBe(true); + }); +}); diff --git a/packages/sdk/src/entities/application-run/utils.ts b/packages/sdk/src/entities/application-run/utils.ts new file mode 100644 index 0000000..421e868 --- /dev/null +++ b/packages/sdk/src/entities/application-run/utils.ts @@ -0,0 +1,74 @@ +import { RunReadResponse } from '../../generated/index.js'; +import { RunStatus } from './types.js'; + +/** + * Compute the overall progress of a run as a percentage (0–100). + * Progress is based on the ratio of terminal items (succeeded + errored + skipped) + * to the total item count. + * + * @param run - Raw run response from the API + * @returns Integer percentage rounded to the nearest whole number + */ +export function getRunProgress(run: RunReadResponse): number { + const { + item_count, + item_succeeded_count, + item_user_error_count, + item_system_error_count, + item_skipped_count, + } = run.statistics; + + // No items means no progress + if (item_count === 0) return 0; + + // Sum all items that have reached a terminal state + const processed = + item_succeeded_count + item_user_error_count + item_system_error_count + item_skipped_count; + return Math.round((processed / item_count) * 100); +} + +/** + * Derive a human-readable {@link RunStatus} from the raw API `state` and `termination_reason`. + * + * Mapping rules: + * - PENDING / PROCESSING → passed through as-is + * - TERMINATED + CANCELED_BY_USER → CANCELED + * - TERMINATED + CANCELED_BY_SYSTEM → FAILED + * - TERMINATED + not all items succeeded → COMPLETED_WITH_ERRORS + * - TERMINATED + all items succeeded → COMPLETED + * + * @param run - Raw run response from the API + */ +export function getRunStatus(run: RunReadResponse): RunStatus { + const { state, statistics, termination_reason } = run; + switch (state) { + case 'PENDING': + return 'PENDING'; + case 'PROCESSING': + return 'PROCESSING'; + case 'TERMINATED': + if (termination_reason === 'CANCELED_BY_USER') { + return 'CANCELED'; + } + if (termination_reason === 'CANCELED_BY_SYSTEM') { + return 'FAILED'; + } + // If any items did not succeed, the run completed with errors + if (statistics.item_count !== statistics.item_succeeded_count) { + return 'COMPLETED_WITH_ERRORS'; + } + return 'COMPLETED'; + } +} + +/** + * Determine whether a run's result items are available for download. + * Items can only be downloaded once the run has reached a completed state. + * + * @param run - Raw run response from the API + * @returns `true` if items are available for download (run is COMPLETED or COMPLETED_WITH_ERRORS) + */ +export const canDownloadRunItems = (run: RunReadResponse): boolean => { + const status = getRunStatus(run); + return !['FAILED', 'CANCELED', 'PENDING', 'PROCESSING'].includes(status); +}; diff --git a/packages/sdk/src/entities/run-item/process-run-item.spec.ts b/packages/sdk/src/entities/run-item/process-run-item.spec.ts new file mode 100644 index 0000000..da6fa5e --- /dev/null +++ b/packages/sdk/src/entities/run-item/process-run-item.spec.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest'; +import { processRunItem } from './process-run-item.js'; +import type { ItemResultReadResponse } from '../../generated/index.js'; + +function buildItem( + overrides: Partial> = {} +): ItemResultReadResponse { + return { + item_id: 'item-1', + external_id: 'slide_1', + custom_metadata: null, + state: overrides.state ?? 'PENDING', + output: 'NONE', + termination_reason: overrides.termination_reason ?? undefined, + error_code: null, + error_message: null, + output_artifacts: [], + } as ItemResultReadResponse; +} + +describe('processRunItem', () => { + it('should preserve all original ItemResultReadResponse fields', () => { + const raw = buildItem({ state: 'PENDING' }); + const result = processRunItem(raw); + + expect(result.item_id).toBe(raw.item_id); + expect(result.external_id).toBe(raw.external_id); + expect(result.state).toBe(raw.state); + expect(result.output_artifacts).toEqual(raw.output_artifacts); + }); + + it('should add status and can_download for a PENDING item', () => { + const result = processRunItem(buildItem({ state: 'PENDING' })); + + expect(result.status).toBe('PENDING'); + expect(result.can_download).toBe(false); + }); + + it('should add status and can_download for a PROCESSING item', () => { + const result = processRunItem(buildItem({ state: 'PROCESSING' })); + + expect(result.status).toBe('PROCESSING'); + expect(result.can_download).toBe(false); + }); + + it('should mark a successfully terminated item as COMPLETED and downloadable', () => { + const result = processRunItem( + buildItem({ state: 'TERMINATED', termination_reason: 'SUCCEEDED' }) + ); + + expect(result.status).toBe('COMPLETED'); + expect(result.can_download).toBe(true); + }); + + it('should mark a SYSTEM_ERROR terminated item as FAILED and not downloadable', () => { + const result = processRunItem( + buildItem({ state: 'TERMINATED', termination_reason: 'SYSTEM_ERROR' }) + ); + + expect(result.status).toBe('FAILED'); + expect(result.can_download).toBe(false); + }); + + it('should mark a USER_ERROR terminated item as FAILED and not downloadable', () => { + const result = processRunItem( + buildItem({ state: 'TERMINATED', termination_reason: 'USER_ERROR' }) + ); + + expect(result.status).toBe('FAILED'); + expect(result.can_download).toBe(false); + }); + + it('should mark a SKIPPED terminated item as SKIPPED and not downloadable', () => { + const result = processRunItem( + buildItem({ state: 'TERMINATED', termination_reason: 'SKIPPED' }) + ); + + expect(result.status).toBe('SKIPPED'); + expect(result.can_download).toBe(false); + }); +}); diff --git a/packages/sdk/src/entities/run-item/process-run-item.ts b/packages/sdk/src/entities/run-item/process-run-item.ts new file mode 100644 index 0000000..4017011 --- /dev/null +++ b/packages/sdk/src/entities/run-item/process-run-item.ts @@ -0,0 +1,21 @@ +import { ItemResultReadResponse } from '../../generated/index.js'; +import { ApplicationRunItem } from './types.js'; +import { canDownloadItem, getItemStatus } from './utils.js'; + +/** + * Transform a raw {@link ItemResultReadResponse} from the API into an enriched + * {@link ApplicationRunItem} by computing derived properties (status, can_download). + * + * This is the single entry-point used by `PlatformSDKHttp.listRunResults()` to + * map API responses before returning them to consumers. + * + * @param item - Raw item response from the API + * @returns Enriched item entity with computed properties spread onto the original response + */ +export const processRunItem = (item: ItemResultReadResponse): ApplicationRunItem => { + return { + ...item, + status: getItemStatus(item), + can_download: canDownloadItem(item), + }; +}; diff --git a/packages/sdk/src/entities/run-item/types.ts b/packages/sdk/src/entities/run-item/types.ts new file mode 100644 index 0000000..bd3d1c1 --- /dev/null +++ b/packages/sdk/src/entities/run-item/types.ts @@ -0,0 +1,18 @@ +import { ItemResultReadResponse } from '../../generated/index.js'; + +/** + * Enriched run-item entity that extends the raw API response + * with computed properties derived from item state and termination reason. + * + * Returned by `PlatformSDKHttp.listRunResults()` instead of the raw + * `ItemResultReadResponse`. + */ +export interface ApplicationRunItem extends ItemResultReadResponse { + /** Whether the item's output artifact is available for download. */ + can_download: boolean; + /** Human-readable status derived from `state` and `termination_reason`. */ + status: string; +} + +/** Derived item status that simplifies the raw `state` + `termination_reason` combination. */ +export type ItemStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'SKIPPED'; diff --git a/packages/sdk/src/entities/run-item/utils.spec.ts b/packages/sdk/src/entities/run-item/utils.spec.ts new file mode 100644 index 0000000..28713a0 --- /dev/null +++ b/packages/sdk/src/entities/run-item/utils.spec.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from 'vitest'; +import { getItemStatus, canDownloadItem } from './utils.js'; +import type { ItemResultReadResponse } from '../../generated/index.js'; + +/** + * Helper to build a minimal ItemResultReadResponse with sensible defaults. + * Only the fields used by the utility functions need to be realistic. + */ +function buildItem( + overrides: Partial> = {} +): ItemResultReadResponse { + return { + item_id: 'item-1', + external_id: 'slide_1', + custom_metadata: null, + state: overrides.state ?? 'PENDING', + output: 'NONE', + termination_reason: overrides.termination_reason ?? undefined, + error_code: null, + error_message: null, + output_artifacts: [], + } as ItemResultReadResponse; +} + +describe('getItemStatus', () => { + it('should return PENDING when state is PENDING', () => { + expect(getItemStatus(buildItem({ state: 'PENDING' }))).toBe('PENDING'); + }); + + it('should return PROCESSING when state is PROCESSING', () => { + expect(getItemStatus(buildItem({ state: 'PROCESSING' }))).toBe('PROCESSING'); + }); + + it('should return COMPLETED when terminated with SUCCEEDED', () => { + expect(getItemStatus(buildItem({ state: 'TERMINATED', termination_reason: 'SUCCEEDED' }))).toBe( + 'COMPLETED' + ); + }); + + it('should return FAILED when terminated with SYSTEM_ERROR', () => { + expect( + getItemStatus(buildItem({ state: 'TERMINATED', termination_reason: 'SYSTEM_ERROR' })) + ).toBe('FAILED'); + }); + + it('should return FAILED when terminated with USER_ERROR', () => { + expect( + getItemStatus(buildItem({ state: 'TERMINATED', termination_reason: 'USER_ERROR' })) + ).toBe('FAILED'); + }); + + it('should return SKIPPED when terminated with SKIPPED', () => { + expect(getItemStatus(buildItem({ state: 'TERMINATED', termination_reason: 'SKIPPED' }))).toBe( + 'SKIPPED' + ); + }); +}); + +describe('canDownloadItem', () => { + it('should return false for PENDING items', () => { + expect(canDownloadItem(buildItem({ state: 'PENDING' }))).toBe(false); + }); + + it('should return false for PROCESSING items', () => { + expect(canDownloadItem(buildItem({ state: 'PROCESSING' }))).toBe(false); + }); + + it('should return true for COMPLETED items', () => { + expect( + canDownloadItem(buildItem({ state: 'TERMINATED', termination_reason: 'SUCCEEDED' })) + ).toBe(true); + }); + + it('should return false for FAILED items (SYSTEM_ERROR)', () => { + expect( + canDownloadItem(buildItem({ state: 'TERMINATED', termination_reason: 'SYSTEM_ERROR' })) + ).toBe(false); + }); + + it('should return false for FAILED items (USER_ERROR)', () => { + expect( + canDownloadItem(buildItem({ state: 'TERMINATED', termination_reason: 'USER_ERROR' })) + ).toBe(false); + }); + + it('should return false for SKIPPED items', () => { + expect(canDownloadItem(buildItem({ state: 'TERMINATED', termination_reason: 'SKIPPED' }))).toBe( + false + ); + }); +}); diff --git a/packages/sdk/src/entities/run-item/utils.ts b/packages/sdk/src/entities/run-item/utils.ts new file mode 100644 index 0000000..01a5786 --- /dev/null +++ b/packages/sdk/src/entities/run-item/utils.ts @@ -0,0 +1,44 @@ +import { ItemResultReadResponse } from '../../generated/index.js'; +import { ItemStatus } from './types.js'; + +/** + * Determine whether a single run item's output artifact is available for download. + * Only items in the COMPLETED state have downloadable artifacts. + * + * @param item - Raw item response from the API + * @returns `true` if the item is in a COMPLETED state + */ +export const canDownloadItem = (item: ItemResultReadResponse): boolean => { + return getItemStatus(item) === 'COMPLETED'; +}; + +/** + * Derive a human-readable {@link ItemStatus} from the raw API `state` and `termination_reason`. + * + * Mapping rules: + * - PENDING / PROCESSING → passed through as-is + * - TERMINATED + SYSTEM_ERROR or USER_ERROR → FAILED + * - TERMINATED + SKIPPED → SKIPPED + * - TERMINATED (otherwise) → COMPLETED + * + * @param item - Raw item response from the API + */ +export function getItemStatus(item: ItemResultReadResponse): ItemStatus { + const { state, termination_reason } = item; + switch (state) { + case 'PENDING': + return 'PENDING'; + case 'PROCESSING': + return 'PROCESSING'; + case 'TERMINATED': + // Items terminated due to errors are marked as FAILED + if (termination_reason === 'SYSTEM_ERROR' || termination_reason === 'USER_ERROR') { + return 'FAILED'; + } + // Explicitly skipped items get their own status + if (termination_reason === 'SKIPPED') { + return 'SKIPPED'; + } + return 'COMPLETED'; + } +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index b7228ec..7ea9f31 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,6 +6,16 @@ export * from './generated/index.js'; // Export error classes export { BaseError, AuthenticationError, APIError, ConfigurationError } from './errors.js'; +export type { ApplicationRun } from './entities/application-run/types.js'; +export { processApplicationRun } from './entities/application-run/process-application-run.js'; +export { + getRunProgress, + getRunStatus, + canDownloadRunItems, +} from './entities/application-run/utils.js'; +export type { ApplicationRunItem, ItemStatus } from './entities/run-item/types.js'; +export { processRunItem } from './entities/run-item/process-run-item.js'; +export { canDownloadItem, getItemStatus } from './entities/run-item/utils.js'; // Export main SDK and types export { PlatformSDKHttp, diff --git a/packages/sdk/src/platform-sdk.test.ts b/packages/sdk/src/platform-sdk.test.ts index 1a27fa6..96cc54d 100644 --- a/packages/sdk/src/platform-sdk.test.ts +++ b/packages/sdk/src/platform-sdk.test.ts @@ -201,6 +201,14 @@ describe('PlatformSDK', () => { expect(result.length).toBeGreaterThan(0); expect(result[0]).toHaveProperty('run_id'); expect(result[0]).toHaveProperty('state'); + + // Enriched ApplicationRun properties + expect(result[0]).toHaveProperty('progress'); + expect(result[0]).toHaveProperty('status'); + expect(result[0]).toHaveProperty('can_download'); + expect(typeof result[0].progress).toBe('number'); + expect(typeof result[0].status).toBe('string'); + expect(typeof result[0].can_download).toBe('boolean'); }); it('should list application runs with filters successfully', async () => { @@ -238,6 +246,14 @@ describe('PlatformSDK', () => { expect(result).toHaveProperty('run_id'); expect(result).toHaveProperty('state'); expect(result).toHaveProperty('version_number'); + + // Enriched ApplicationRun properties + expect(result).toHaveProperty('progress'); + expect(result).toHaveProperty('status'); + expect(result).toHaveProperty('can_download'); + expect(typeof result.progress).toBe('number'); + expect(typeof result.status).toBe('string'); + expect(typeof result.can_download).toBe('boolean'); }); it('should handle get run failure', async () => { @@ -284,6 +300,12 @@ describe('PlatformSDK', () => { expect(result[0]).toHaveProperty('item_id'); expect(result[0]).toHaveProperty('state'); expect(result[0]).toHaveProperty('external_id'); + + // Enriched ApplicationRunItem properties + expect(result[0]).toHaveProperty('status'); + expect(result[0]).toHaveProperty('can_download'); + expect(typeof result[0].status).toBe('string'); + expect(typeof result[0].can_download).toBe('boolean'); }); it('should list run results failure', async () => { diff --git a/packages/sdk/src/platform-sdk.ts b/packages/sdk/src/platform-sdk.ts index 459e9c4..fa9a40a 100644 --- a/packages/sdk/src/platform-sdk.ts +++ b/packages/sdk/src/platform-sdk.ts @@ -1,7 +1,5 @@ import packageJson from '../package.json' with { type: 'json' }; import { - RunReadResponse, - ItemResultReadResponse, RunCreationRequest, RunCreationResponse, PublicApi, @@ -14,6 +12,10 @@ import { import { APIError, AuthenticationError, UnexpectedError } from './errors.js'; import { isAxiosError } from 'axios'; import z from 'zod'; +import { processApplicationRun } from './entities/application-run/process-application-run.js'; +import { ApplicationRun } from './entities/application-run/types.js'; +import { processRunItem } from './entities/run-item/process-run-item.js'; +import { ApplicationRunItem } from './entities/run-item/types.js'; const validationErrorSchema = z.object({ detail: z.array( @@ -100,9 +102,9 @@ export interface PlatformSDK { pageSize?: number; customMetadata?: string; sort?: string[]; - }): Promise; + }): Promise; createApplicationRun(request: RunCreationRequest): Promise; - getRun(applicationRunId: string): Promise; + getRun(applicationRunId: string): Promise; cancelApplicationRun(applicationRunId: string): Promise; listRunResults( applicationRunId: string, @@ -114,7 +116,7 @@ export interface PlatformSDK { state?: ItemState; terminationReason?: ItemTerminationReason; } - ): Promise; + ): Promise; getApplicationVersionDetails( applicationId: string, version: string @@ -357,7 +359,7 @@ export class PlatformSDKHttp implements PlatformSDK { sort?: string[]; page?: number; pageSize?: number; - }): Promise { + }): Promise { const client = await this.#getClient(); try { const response = await client.listRunsV1RunsGet({ @@ -369,7 +371,8 @@ export class PlatformSDKHttp implements PlatformSDK { pageSize: options?.pageSize, }); - return response.data; + // Enrich each raw RunReadResponse with computed properties + return response.data.map(processApplicationRun); } catch (error) { handleRequestError(error); } @@ -450,14 +453,15 @@ export class PlatformSDKHttp implements PlatformSDK { * } * ``` */ - async getRun(applicationRunId: string): Promise { + async getRun(applicationRunId: string): Promise { const client = await this.#getClient(); try { const response = await client.getRunV1RunsRunIdGet({ runId: applicationRunId, }); - return response.data; + // Enrich raw RunReadResponse with computed properties + return processApplicationRun(response.data); } catch (error) { handleRequestError(error); } @@ -548,7 +552,7 @@ export class PlatformSDKHttp implements PlatformSDK { state?: ItemState; terminationReason?: ItemTerminationReason; } = {} - ): Promise { + ): Promise { const client = await this.#getClient(); try { const response = await client.listRunItemsV1RunsRunIdItemsGet({ @@ -561,7 +565,8 @@ export class PlatformSDKHttp implements PlatformSDK { terminationReason, }); - return response.data; + // Enrich each raw ItemResultReadResponse with computed properties + return response.data.map(processRunItem); } catch (error) { handleRequestError(error); }