Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* {@code ScriptExecutionRepository}.
*/
@Repository
public interface CommandExecutionRepository extends MongoRepository<CommandExecution, String> {
public interface CommandExecutionRepository extends MongoRepository<CommandExecution, String>, CustomCommandExecutionRepository {

/** Tenant-scoped lookup by Mongo {@code _id} — backs Relay {@code node(id)} refetch. */
Optional<CommandExecution> findByTenantIdAndId(String tenantId, String id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.openframe.data.repository.rmm;

import com.openframe.data.document.rmm.CommandExecution;

public interface CustomCommandExecutionRepository {

void applyResult(CommandExecution row);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.openframe.data.repository.rmm;

import com.openframe.data.document.rmm.CommandExecution;
import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class CustomCommandExecutionRepositoryImpl implements CustomCommandExecutionRepository {

private static final String FIELD_ID = "_id";

private final MongoTemplate mongoTemplate;

@Override
public void applyResult(CommandExecution row) {
Update update = new Update()
.set("status", row.getStatus())
.set("statusChangedAt", row.getStatusChangedAt())
.set("finishedAt", row.getFinishedAt())
.set("exitCode", row.getExitCode())
.set("executionTimeMs", row.getExecutionTimeMs())
.set("timedOut", row.getTimedOut())
.set("stdout", row.getStdout())
.set("stdoutTruncated", row.getStdoutTruncated())
.set("stderr", row.getStderr())
.set("stderrTruncated", row.getStderrTruncated())
.set("error", row.getError());
mongoTemplate.updateFirst(new Query(Criteria.where(FIELD_ID).is(row.getId())), update, CommandExecution.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,11 @@
* Custom MongoTemplate-backed queries for {@link ScriptExecution}. Owner-scoped —
* both per-script (Script → Execution History tab) and per-schedule (Schedule →
* Execution History tab) queries share one API, differing only in {@link ExecutionOwnerScope}.
*
* <p>Spring Data derived methods cannot express id-cursor pagination + facet aggregations
* over shared predicate variations, so the list / count / facet queries live here rather
* than in the service — mirrors {@code CustomScriptRepository}. Implementation:
* {@code CustomScriptExecutionRepositoryImpl}.
*/
public interface CustomScriptExecutionRepository {

/**
* Cursor-paginated executions for one owner (script or schedule) within a tenant.
*
* <p>Cursor semantics: the cursor is the raw {@code ObjectId} hex from the boundary
* row of the previous page. With {@code backward=true} the scan walks in the
* opposite direction so paging "before" a cursor returns the rows immediately newer
* than it; the caller is expected to reverse the returned list for display.
*
* <p>Pass {@code limit + 1} from the caller to detect whether more pages exist
* beyond this one (the canonical "fetch one extra" trick).
*
* @param tenantId tenant scope — required, never null
* @param owner what narrows the base predicate — see {@link ExecutionOwnerScope}
* @param filter optional extra constraints (statuses/initiators/machines); null / empty
* fields impose no constraint
* @param sortField sort field, must satisfy {@link #isSortableField}
* @param sortDirection sort direction
* @param cursor raw {@code _id} cursor (already base64-decoded); null = first page
* @param backward true when paginating with {@code before/last}
* @param limit max rows to return (usually {@code pageSize + 1})
* @param search optional case-insensitive substring matched across
* {@code executionId}, {@code machineId}, {@code stdout},
* {@code stderr}; null/blank imposes no constraint
*/
void applyResult(ScriptExecution row);

List<ScriptExecution> findPage(String tenantId,
ExecutionOwnerScope owner,
ScriptExecutionQueryFilter filter,
Expand All @@ -55,45 +28,21 @@ List<ScriptExecution> findPage(String tenantId,
int limit,
String search);

/**
* Full matching count for the {@code (tenantId, owner, filter, search)} tuple, ignoring
* pagination. Backs the connection's {@code filteredCount} so the UI can show the full
* total immediately while items load page by page.
*/
long count(String tenantId, ExecutionOwnerScope owner, ScriptExecutionQueryFilter filter, String search);

/**
* Faceted options for one owner's Execution History: {@code value → matching count}.
* Applies every filter arm EXCEPT the facet's own field, so its dropdown keeps offering
* every switchable value. Labels are resolved by the service.
*/
Map<String, Integer> facet(String tenantId,
ExecutionOwnerScope owner,
ScriptExecutionQueryFilter filter,
String search,
ExecutionFacetField facet);

/** Whether the given field is allowed as a sort key. */
boolean isSortableField(String field);

/** Default sort field when none is supplied. */
String getDefaultSortField();

/**
* Encode the compound-keyset cursor for a page boundary row: {@code <sortValue>|<hexId>}
* for non-{@code _id} sort fields (empty {@code sortValue} for null Instant), or the plain
* hex {@code _id} for {@code _id} sort. Consumed by {@link #findPage} on the next request.
*/
String encodeCursor(com.openframe.data.document.rmm.ScriptExecution row, String sortField);

/**
* Count leaf {@link ScriptExecution} rows for one schedule fire, grouped by status.
* A single {@code $match + $group} pass — one round-trip — backs the header aggregator
* so it can decide "any leaf still running? any failed?" without loading the rows
* themselves. Tenant-scoped so it hits the compound index.
*/
LeafStatusCounts countLeavesByStatus(String tenantId, String executionId);

/** Running/failed counts for the leaves of one schedule fire; other terminal statuses are irrelevant to the decision. */
record LeafStatusCounts(long running, long failed) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;

import java.time.Instant;
Expand Down Expand Up @@ -83,6 +84,23 @@ public class CustomScriptExecutionRepositoryImpl implements CustomScriptExecutio

private final MongoTemplate mongoTemplate;

@Override
public void applyResult(ScriptExecution row) {
Update update = new Update()
.set(FIELD_STATUS, row.getStatus())
.set(FIELD_STATUS_CHANGED_AT, row.getStatusChangedAt())
.set(FIELD_FINISHED_AT, row.getFinishedAt())
.set("exitCode", row.getExitCode())
.set("executionTimeMs", row.getExecutionTimeMs())
.set("timedOut", row.getTimedOut())
.set(FIELD_STDOUT, row.getStdout())
.set("stdoutTruncated", row.getStdoutTruncated())
.set(FIELD_STDERR, row.getStderr())
.set("stderrTruncated", row.getStderrTruncated())
.set("error", row.getError());
mongoTemplate.updateFirst(new Query(Criteria.where(FIELD_ID).is(row.getId())), update, ScriptExecution.class);
}

@Override
public List<ScriptExecution> findPage(String tenantId, ExecutionOwnerScope owner,
ScriptExecutionQueryFilter filter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ private void applyResult(CommandExecution row, JsonNode after) {
row.setStderr(truncStderr.value);
row.setStderrTruncated(truncStderr.truncated);

commandExecutionRepository.save(row);
commandExecutionRepository.applyResult(row);
log.info("Transitioned CommandExecution row: executionId={} machineId={} status=RUNNING→{} exitCode={} timedOut={}",
row.getExecutionId(), row.getMachineId(), newStatus, exitCode, timedOut);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ private void applyResult(ScriptExecution row, JsonNode after) {
row.setStderr(truncStderr.value);
row.setStderrTruncated(truncStderr.truncated);

scriptExecutionRepository.save(row);
scriptExecutionRepository.applyResult(row);
log.info("Transitioned Execution row: executionId={} status=RUNNING→{} exitCode={} timedOut={}",
row.getExecutionId(), newStatus, exitCode, timedOut);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void handle_success_transitionsRowToSuccess() {
handler.handle(messageWith(0, false, null, 42L, "ok\n", ""), new IntegratedToolEnrichedData());

ArgumentCaptor<CommandExecution> captor = ArgumentCaptor.forClass(CommandExecution.class);
verify(commandExecutionRepository).save(captor.capture());
verify(commandExecutionRepository).applyResult(captor.capture());
CommandExecution saved = captor.getValue();
assertThat(saved.getStatus()).isEqualTo(ExecutionStatus.SUCCESS);
assertThat(saved.getExitCode()).isZero();
Expand All @@ -87,7 +87,7 @@ void handle_nonZeroExit_transitionsRowToFailing() {
handler.handle(messageWith(1, false, null, null, null, null), new IntegratedToolEnrichedData());

ArgumentCaptor<CommandExecution> captor = ArgumentCaptor.forClass(CommandExecution.class);
verify(commandExecutionRepository).save(captor.capture());
verify(commandExecutionRepository).applyResult(captor.capture());
assertThat(captor.getValue().getStatus()).isEqualTo(ExecutionStatus.FAILED);
}

Expand All @@ -101,7 +101,7 @@ void handle_timedOut_transitionsRowToFailing() {
handler.handle(messageWith(null, true, null, null, null, null), new IntegratedToolEnrichedData());

ArgumentCaptor<CommandExecution> captor = ArgumentCaptor.forClass(CommandExecution.class);
verify(commandExecutionRepository).save(captor.capture());
verify(commandExecutionRepository).applyResult(captor.capture());
assertThat(captor.getValue().getStatus()).isEqualTo(ExecutionStatus.FAILED);
assertThat(captor.getValue().getTimedOut()).isTrue();
}
Expand All @@ -116,7 +116,7 @@ void handle_agentError_transitionsRowToFailing() {
handler.handle(messageWith(0, false, "SHELL_UNAVAILABLE", null, null, null), new IntegratedToolEnrichedData());

ArgumentCaptor<CommandExecution> captor = ArgumentCaptor.forClass(CommandExecution.class);
verify(commandExecutionRepository).save(captor.capture());
verify(commandExecutionRepository).applyResult(captor.capture());
assertThat(captor.getValue().getStatus()).isEqualTo(ExecutionStatus.FAILED);
assertThat(captor.getValue().getError()).isEqualTo("SHELL_UNAVAILABLE");
}
Expand All @@ -131,7 +131,7 @@ void handle_alreadyTerminal_doesNotOverwrite() {

handler.handle(messageWith(0, false, null, null, null, null), new IntegratedToolEnrichedData());

verify(commandExecutionRepository, never()).save(any());
verify(commandExecutionRepository, never()).applyResult(any());
}

@Test
Expand All @@ -142,7 +142,7 @@ void handle_rowMissing_skipsSaveQuietly() {

handler.handle(messageWith(0, false, null, null, null, null), new IntegratedToolEnrichedData());

verify(commandExecutionRepository, never()).save(any());
verify(commandExecutionRepository, never()).applyResult(any());
}

@Test
Expand All @@ -156,7 +156,7 @@ void handle_truncatesLargeStdoutAndStderr() {
handler.handle(messageWith(0, false, null, null, huge, huge), new IntegratedToolEnrichedData());

ArgumentCaptor<CommandExecution> captor = ArgumentCaptor.forClass(CommandExecution.class);
verify(commandExecutionRepository).save(captor.capture());
verify(commandExecutionRepository).applyResult(captor.capture());
CommandExecution saved = captor.getValue();
assertThat(saved.getStdout().getBytes(StandardCharsets.UTF_8).length)
.isLessThanOrEqualTo(CommandExecution.MAX_OUTPUT_BYTES);
Expand Down
Loading
Loading