Skip to content

Commit 5a1ccba

Browse files
Vitexusclaude
andcommitted
feat(job): implement concrete JobApi using job_output_lines table
Add JobApi extending AbstractJobApi with PDO injected from DI container: - getjobById: returns job row + assembled stdout/stderr strings + full output_lines array (skipped when ?output=false) - listjobs: paginated job list without output (perf); supports limit/offset/order query params - setjobById: create or update job record (output is written by the executor, not the API) stdout/stderr are assembled from job_output_lines; the jobs table no longer contains those columns after the migration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cba2ff3 commit 5a1ccba

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

lib/Server/JobApi.php

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the MultiFlexi package
7+
*
8+
* https://multiflexi.eu/
9+
*
10+
* (c) Vítězslav Dvořák <http://vitexsoftware.com>
11+
*
12+
* For the full copyright and license information, please view the LICENSE
13+
* file that was distributed with this source code.
14+
*/
15+
16+
namespace MultiFlexi\Api\Server;
17+
18+
use Psr\Http\Message\ResponseInterface;
19+
use Psr\Http\Message\ServerRequestInterface;
20+
21+
/**
22+
* Concrete Job API implementation.
23+
*
24+
* Reads job data and output lines directly via PDO so this package stays
25+
* independent of multiflexi-core. Output is served from job_output_lines;
26+
* the jobs table no longer contains stdout/stderr columns.
27+
*/
28+
class JobApi extends AbstractJobApi
29+
{
30+
private \PDO $pdo;
31+
32+
public function __construct(\PDO $pdo)
33+
{
34+
$this->pdo = $pdo;
35+
}
36+
37+
/**
38+
* GET /job/{jobId}.{suffix} — Return a single job with its output lines.
39+
*/
40+
public function getjobById(
41+
ServerRequestInterface $request,
42+
ResponseInterface $response,
43+
int $jobId,
44+
string $suffix
45+
): ResponseInterface {
46+
$job = $this->fetchJob($jobId);
47+
48+
if ($job === null) {
49+
return $response->withStatus(404);
50+
}
51+
52+
$queryParams = $request->getQueryParams();
53+
$includeOutput = filter_var($queryParams['output'] ?? 'true', \FILTER_VALIDATE_BOOLEAN);
54+
55+
if ($includeOutput) {
56+
$job['stdout'] = $this->assembleOutput($jobId, 'stdout');
57+
$job['stderr'] = $this->assembleOutput($jobId, 'stderr');
58+
$job['output_lines'] = $this->fetchOutputLines($jobId);
59+
}
60+
61+
$response->getBody()->write((string) json_encode($job));
62+
63+
return $response->withHeader('Content-Type', 'application/json');
64+
}
65+
66+
/**
67+
* GET /jobs.{suffix} — List jobs (without output for performance).
68+
*/
69+
public function listjobs(
70+
ServerRequestInterface $request,
71+
ResponseInterface $response,
72+
string $suffix
73+
): ResponseInterface {
74+
$queryParams = $request->getQueryParams();
75+
$limit = isset($queryParams['limit']) ? (int) $queryParams['limit'] : 100;
76+
$offset = isset($queryParams['offset']) ? (int) $queryParams['offset'] : 0;
77+
$order = strtoupper($queryParams['order'] ?? 'D') === 'A' ? 'ASC' : 'DESC';
78+
79+
$limit = max(1, min($limit, 1000));
80+
$offset = max(0, $offset);
81+
82+
$stmt = $this->pdo->prepare(
83+
"SELECT id, app_id, company_id, runtemplate_id, executor, exitcode,
84+
begin, end, schedule, schedule_type, launched_by, app_version,
85+
pid, task_id
86+
FROM job
87+
ORDER BY id {$order}
88+
LIMIT :limit OFFSET :offset",
89+
);
90+
$stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
91+
$stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
92+
$stmt->execute();
93+
94+
$jobs = $stmt->fetchAll(\PDO::FETCH_ASSOC);
95+
96+
$response->getBody()->write((string) json_encode($jobs));
97+
98+
return $response->withHeader('Content-Type', 'application/json');
99+
}
100+
101+
/**
102+
* POST /job/ — Create or update a job record.
103+
*/
104+
public function setjobById(
105+
ServerRequestInterface $request,
106+
ResponseInterface $response
107+
): ResponseInterface {
108+
$body = (array) ($request->getParsedBody() ?? []);
109+
110+
if (empty($body)) {
111+
return $response->withStatus(400);
112+
}
113+
114+
// Allowed writable fields (stdout/stderr are no longer columns)
115+
$allowed = ['app_id', 'company_id', 'runtemplate_id', 'executor', 'exitcode',
116+
'begin', 'end', 'schedule', 'schedule_type', 'launched_by', 'app_version',
117+
'pid', 'task_id', 'env', 'command'];
118+
119+
$data = array_intersect_key($body, array_flip($allowed));
120+
121+
if (isset($body['id']) && (int) $body['id'] > 0) {
122+
// Update
123+
$jobId = (int) $body['id'];
124+
125+
if (!$this->fetchJob($jobId)) {
126+
return $response->withStatus(404);
127+
}
128+
129+
if (!empty($data)) {
130+
$setClauses = implode(', ', array_map(static fn ($k) => "{$k} = :{$k}", array_keys($data)));
131+
$stmt = $this->pdo->prepare("UPDATE job SET {$setClauses} WHERE id = :id");
132+
$stmt->bindValue(':id', $jobId, \PDO::PARAM_INT);
133+
134+
foreach ($data as $key => $value) {
135+
$stmt->bindValue(":{$key}", $value);
136+
}
137+
138+
$stmt->execute();
139+
}
140+
} else {
141+
// Insert
142+
if (empty($data)) {
143+
return $response->withStatus(400);
144+
}
145+
146+
$cols = implode(', ', array_keys($data));
147+
$placeholders = implode(', ', array_map(static fn ($k) => ":{$k}", array_keys($data)));
148+
$stmt = $this->pdo->prepare("INSERT INTO job ({$cols}) VALUES ({$placeholders})");
149+
150+
foreach ($data as $key => $value) {
151+
$stmt->bindValue(":{$key}", $value);
152+
}
153+
154+
$stmt->execute();
155+
$jobId = (int) $this->pdo->lastInsertId();
156+
}
157+
158+
$job = $this->fetchJob($jobId);
159+
$response->getBody()->write((string) json_encode($job));
160+
161+
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
162+
}
163+
164+
// -----------------------------------------------------------------------
165+
// Private helpers
166+
// -----------------------------------------------------------------------
167+
168+
private function fetchJob(int $jobId): ?array
169+
{
170+
$stmt = $this->pdo->prepare(
171+
'SELECT id, app_id, company_id, runtemplate_id, executor, exitcode,
172+
begin, end, schedule, schedule_type, launched_by, app_version,
173+
pid, task_id
174+
FROM job WHERE id = :id',
175+
);
176+
$stmt->bindValue(':id', $jobId, \PDO::PARAM_INT);
177+
$stmt->execute();
178+
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
179+
180+
return $row !== false ? $row : null;
181+
}
182+
183+
/**
184+
* Concatenate all lines of a given type for a job.
185+
*/
186+
private function assembleOutput(int $jobId, string $type): string
187+
{
188+
$stmt = $this->pdo->prepare(
189+
'SELECT line FROM job_output_lines
190+
WHERE job_id = :job_id AND type = :type
191+
ORDER BY seq ASC, id ASC',
192+
);
193+
$stmt->bindValue(':job_id', $jobId, \PDO::PARAM_INT);
194+
$stmt->bindValue(':type', $type);
195+
$stmt->execute();
196+
197+
return implode('', array_column($stmt->fetchAll(\PDO::FETCH_ASSOC), 'line'));
198+
}
199+
200+
/**
201+
* Return all output lines for a job (all types), ordered by sequence.
202+
*/
203+
private function fetchOutputLines(int $jobId): array
204+
{
205+
$stmt = $this->pdo->prepare(
206+
'SELECT id, seq, type, line, created_at
207+
FROM job_output_lines
208+
WHERE job_id = :job_id
209+
ORDER BY seq ASC, id ASC',
210+
);
211+
$stmt->bindValue(':job_id', $jobId, \PDO::PARAM_INT);
212+
$stmt->execute();
213+
214+
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
215+
}
216+
}

0 commit comments

Comments
 (0)