Skip to content

Commit c5d9909

Browse files
feat(jobs): add Jobs.getByKey method [PLT-100298]
Exposes the existing GET_BY_KEY endpoint as a public getByKey() method on the Jobs service. Refactors getOutput() to use getByKey() internally instead of the private fetchJobByKey() helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8023fbe commit c5d9909

7 files changed

Lines changed: 289 additions & 45 deletions

File tree

docs/oauth-scopes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This page lists the specific OAuth scopes required in external app for each SDK
1414
| Method | OAuth Scope |
1515
|--------|-------------|
1616
| `getAll()` | `OR.Jobs` or `OR.Jobs.Read` |
17+
| `getByKey()` | `OR.Jobs` or `OR.Jobs.Read` |
1718
| `getOutput()` | `OR.Jobs` or `OR.Jobs.Read` |
1819

1920
## Attachments

src/models/orchestrator/jobs.internal-types.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/models/orchestrator/jobs.models.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JobGetAllOptions, RawJobGetResponse } from './jobs.types';
1+
import { JobGetAllOptions, JobGetByKeyOptions, RawJobGetResponse } from './jobs.types';
22
import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination';
33

44
// Combined type for job data with methods
@@ -67,6 +67,35 @@ export interface JobServiceModel {
6767
: NonPaginatedResponse<JobGetResponse>
6868
>;
6969

70+
/**
71+
* Gets a job by its unique key (GUID).
72+
*
73+
* Returns the full job details including state, timing, input/output arguments, and error information.
74+
* Use `expand` to include related entities like Robot, Machine, or Release.
75+
*
76+
* @param jobKey - The unique key (GUID) of the job to retrieve
77+
* @param folderId - The folder ID where the job resides
78+
* @param options - Optional query options for expanding or selecting fields
79+
* @returns Promise resolving to a {@link JobGetResponse} with full job details and bound methods
80+
*
81+
* @example
82+
* ```typescript
83+
* // Get a job by key
84+
* const job = await jobs.getByKey(<jobKey>, <folderId>);
85+
* console.log(job.state, job.processName);
86+
* ```
87+
*
88+
* @example
89+
* ```typescript
90+
* // With expanded related entities
91+
* const job = await jobs.getByKey(<jobKey>, <folderId>, {
92+
* expand: 'Robot,Machine,Release'
93+
* });
94+
* console.log(job.robot?.name, job.machine?.name);
95+
* ```
96+
*/
97+
getByKey(jobKey: string, folderId: number, options?: JobGetByKeyOptions): Promise<JobGetResponse>;
98+
7099
/**
71100
* Gets the output of a completed job.
72101
*

src/models/orchestrator/jobs.types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JobState, RequestOptions } from '../common/types';
1+
import { JobState, BaseOptions, RequestOptions } from '../common/types';
22
import { PaginationOptions } from '../../utils/pagination';
33
import {
44
JobPriority,
@@ -166,3 +166,8 @@ export type JobGetAllOptions = RequestOptions & PaginationOptions & {
166166
folderId?: number;
167167
}
168168

169+
/**
170+
* Options for getting a job by key
171+
*/
172+
export interface JobGetByKeyOptions extends BaseOptions {}
173+

src/services/orchestrator/jobs/jobs.ts

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { FolderScopedService } from '../../folder-scoped';
2-
import { RawJobGetResponse, JobGetAllOptions } from '../../../models/orchestrator/jobs.types';
3-
import { RawJobOutputFields } from '../../../models/orchestrator/jobs.internal-types';
2+
import { RawJobGetResponse, JobGetAllOptions, JobGetByKeyOptions } from '../../../models/orchestrator/jobs.types';
43
import { JobServiceModel, JobGetResponse, createJobWithMethods } from '../../../models/orchestrator/jobs.models';
5-
import { pascalToCamelCaseKeys, transformData } from '../../../utils/transform';
4+
import { addPrefixToKeys, pascalToCamelCaseKeys, transformData } from '../../../utils/transform';
65
import { JOB_ENDPOINTS } from '../../../utils/constants/endpoints';
7-
import { ODATA_PAGINATION, ODATA_OFFSET_PARAMS } from '../../../utils/constants/common';
6+
import { ODATA_PAGINATION, ODATA_OFFSET_PARAMS, ODATA_PREFIX } from '../../../utils/constants/common';
87
import { JobMap } from '../../../models/orchestrator/jobs.constants';
98
import { AttachmentService } from '../attachments/attachments';
109
import { ValidationError, ServerError } from '../../../core/errors';
@@ -105,6 +104,55 @@ export class JobService extends FolderScopedService implements JobServiceModel {
105104
}, options) as any;
106105
}
107106

107+
/**
108+
* Gets a job by its unique key (GUID).
109+
*
110+
* Returns the full job details including state, timing, input/output arguments, and error information.
111+
* Use `expand` to include related entities like Robot, Machine, or Release.
112+
*
113+
* @param jobKey - The unique key (GUID) of the job to retrieve
114+
* @param folderId - The folder ID where the job resides
115+
* @param options - Optional query options for expanding or selecting fields
116+
* @returns Promise resolving to a {@link JobGetResponse} with full job details and bound methods
117+
*
118+
* @example
119+
* ```typescript
120+
* // Get a job by key
121+
* const job = await jobs.getByKey(<jobKey>, <folderId>);
122+
* console.log(job.state, job.processName);
123+
* ```
124+
*
125+
* @example
126+
* ```typescript
127+
* // With expanded related entities
128+
* const job = await jobs.getByKey(<jobKey>, <folderId>, {
129+
* expand: 'Robot,Machine,Release'
130+
* });
131+
* console.log(job.robot?.name, job.machine?.name);
132+
* ```
133+
*/
134+
@track('Jobs.GetByKey')
135+
async getByKey(jobKey: string, folderId: number, options: JobGetByKeyOptions = {}): Promise<JobGetResponse> {
136+
if (!jobKey) {
137+
throw new ValidationError({ message: 'jobKey is required for getByKey' });
138+
}
139+
140+
const headers = createHeaders({ [FOLDER_ID]: folderId });
141+
const keysToPrefix = Object.keys(options);
142+
const apiOptions = addPrefixToKeys(options, ODATA_PREFIX, keysToPrefix);
143+
144+
const response = await this.get<Record<string, unknown>>(
145+
JOB_ENDPOINTS.GET_BY_KEY(jobKey),
146+
{
147+
params: apiOptions,
148+
headers,
149+
}
150+
);
151+
152+
const rawJob = transformData(pascalToCamelCaseKeys(response.data) as RawJobGetResponse, JobMap);
153+
return createJobWithMethods(rawJob, this);
154+
}
155+
108156
/**
109157
* Gets the output of a completed job.
110158
*
@@ -143,44 +191,23 @@ export class JobService extends FolderScopedService implements JobServiceModel {
143191
throw new ValidationError({ message: 'jobKey is required for getOutput' });
144192
}
145193

146-
const job = await this.fetchJobByKey(jobKey, folderId);
194+
const job = await this.getByKey(jobKey, folderId, { select: 'OutputArguments,OutputFile' });
147195

148-
if (job.OutputArguments) {
196+
if (job.outputArguments) {
149197
try {
150-
return JSON.parse(job.OutputArguments) as Record<string, unknown>;
198+
return JSON.parse(job.outputArguments) as Record<string, unknown>;
151199
} catch {
152200
throw new ServerError({ message: 'Failed to parse job output arguments as JSON' });
153201
}
154202
}
155203

156-
if (job.OutputFile) {
157-
return this.downloadOutputFile(job.OutputFile);
204+
if (job.outputFile) {
205+
return this.downloadOutputFile(job.outputFile);
158206
}
159207

160208
return null;
161209
}
162210

163-
/**
164-
* Fetches a job by its Key (GUID) using the GetByKey endpoint.
165-
* Only selects fields needed for output extraction.
166-
*/
167-
private async fetchJobByKey(
168-
jobKey: string,
169-
folderId: number
170-
): Promise<RawJobOutputFields> {
171-
const headers = createHeaders({ [FOLDER_ID]: folderId });
172-
const response = await this.get<RawJobOutputFields>(
173-
JOB_ENDPOINTS.GET_BY_KEY(jobKey),
174-
{
175-
params: {
176-
$select: 'OutputArguments,OutputFile',
177-
},
178-
headers,
179-
}
180-
);
181-
return response.data;
182-
}
183-
184211
/**
185212
* Downloads the output file content via the Attachments API.
186213
* 1. Fetches blob access info from the attachment using AttachmentService

tests/integration/shared/orchestrator/jobs.integration.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,113 @@ describe.each(modes)('Orchestrator Jobs - Integration Tests [%s]', (mode) => {
6262
});
6363
});
6464

65+
describe('getByKey', () => {
66+
it('should retrieve a job by key', async () => {
67+
const { jobs, folderId } = getJobsService();
68+
69+
if (!folderId) {
70+
throw new Error('INTEGRATION_TEST_FOLDER_ID is required for getByKey tests.');
71+
}
72+
73+
// First get a job key from getAll
74+
const allJobs = await jobs.getAll({
75+
folderId,
76+
pageSize: 1,
77+
});
78+
79+
if (allJobs.items.length === 0) {
80+
throw new Error('No jobs available to test getByKey.');
81+
}
82+
83+
const jobKey = allJobs.items[0].key;
84+
const job = await jobs.getByKey(jobKey, folderId);
85+
86+
expect(job).toBeDefined();
87+
expect(job.id).toBeDefined();
88+
expect(job.key).toBe(jobKey);
89+
expect(job.state).toBeDefined();
90+
expect(typeof job.id).toBe('number');
91+
});
92+
93+
it('should retrieve a job with expand options', async () => {
94+
const { jobs, folderId } = getJobsService();
95+
96+
if (!folderId) {
97+
throw new Error('INTEGRATION_TEST_FOLDER_ID is required for getByKey tests.');
98+
}
99+
100+
const allJobs = await jobs.getAll({
101+
folderId,
102+
pageSize: 1,
103+
});
104+
105+
if (allJobs.items.length === 0) {
106+
throw new Error('No jobs available to test getByKey with expand.');
107+
}
108+
109+
const jobKey = allJobs.items[0].key;
110+
const job = await jobs.getByKey(jobKey, folderId, {
111+
expand: 'Robot,Machine,Release',
112+
});
113+
114+
expect(job).toBeDefined();
115+
expect(job.key).toBe(jobKey);
116+
});
117+
118+
it('should have bound getOutput method on result', async () => {
119+
const { jobs, folderId } = getJobsService();
120+
121+
if (!folderId) {
122+
throw new Error('INTEGRATION_TEST_FOLDER_ID is required for getByKey tests.');
123+
}
124+
125+
const allJobs = await jobs.getAll({
126+
folderId,
127+
pageSize: 1,
128+
});
129+
130+
if (allJobs.items.length === 0) {
131+
throw new Error('No jobs available to test getByKey bound methods.');
132+
}
133+
134+
const jobKey = allJobs.items[0].key;
135+
const job = await jobs.getByKey(jobKey, folderId);
136+
137+
expect(job.getOutput).toBeDefined();
138+
expect(typeof job.getOutput).toBe('function');
139+
});
140+
141+
it('should have transformed camelCase fields and no PascalCase fields', async () => {
142+
const { jobs, folderId } = getJobsService();
143+
144+
if (!folderId) {
145+
throw new Error('INTEGRATION_TEST_FOLDER_ID is required for getByKey transform tests.');
146+
}
147+
148+
const allJobs = await jobs.getAll({
149+
folderId,
150+
pageSize: 1,
151+
});
152+
153+
if (allJobs.items.length === 0) {
154+
throw new Error('No jobs available to validate transform.');
155+
}
156+
157+
const jobKey = allJobs.items[0].key;
158+
const job = await jobs.getByKey(jobKey, folderId);
159+
160+
// Verify transformed camelCase fields exist
161+
expect(job.createdTime).toBeDefined();
162+
expect(job.processName).toBeDefined();
163+
expect(job.folderId).toBeDefined();
164+
165+
// Verify original PascalCase API fields are absent
166+
expect((job as any).CreationTime).toBeUndefined();
167+
expect((job as any).ReleaseName).toBeUndefined();
168+
expect((job as any).OrganizationUnitId).toBeUndefined();
169+
});
170+
});
171+
65172
describe('getOutput', () => {
66173
it('should return parsed output or null for a completed job', async () => {
67174
const { jobs, folderId } = getJobsService();

0 commit comments

Comments
 (0)