Skip to content

Commit 09fbef6

Browse files
authored
Implement rest api to list publishing jobs - #34319 (#34344)
### Proposed Changes - Add GET /v1/publishing endpoint to list publishing jobs with pagination, status filtering, and text search on bundle ID/name - Extend PublishAuditAPI with filtered query methods supporting status list and text filter with JOIN to publishing_bundle table - Add integration tests and Postman sanity tests for endpoint validation ### Checklist - [ ] Tests - [ ] Translations - [ ] Security Implications Contemplated (add notes if applicable) ### Additional Info ** any additional useful context or info ** ### Screenshots Original | Updated :-------------------------:|:-------------------------: ** original screenshot ** | ** updated screenshot ** This PR fixes: #34319
1 parent 7a70905 commit 09fbef6

File tree

11 files changed

+1934
-2
lines changed

11 files changed

+1934
-2
lines changed

dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPI.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,31 @@ public abstract List<String> getBundleIdByStatus(final List<Status> statusList,
211211
public abstract List<String> getBundleIdByStatusFilterByOwner(final List<Status> statusList, final int limit, final int offset, final String userId)
212212
throws DotDataException;
213213

214+
/**
215+
* Get {@link PublishAuditStatus} paginated with combined text and status filtering.
216+
* The text filter searches both bundle_id and bundle name (via JOIN with publishing_bundle table).
217+
*
218+
* @param limit limit of rows for retrieved page
219+
* @param offset offset of rows for retrieved page
220+
* @param limitAssets max limit of assets to retrieve for each {@link PublishAuditStatus}
221+
* @param filter case-insensitive partial match filter on bundle_id and bundle name, or null for no text filter
222+
* @param statusList list of statuses to filter by, or empty/null for all statuses
223+
* @return List of {@link PublishAuditStatus}
224+
* @throws DotPublisherException if any error occurs
225+
*/
226+
public abstract List<PublishAuditStatus> getPublishAuditStatus(
227+
int limit, int offset, int limitAssets, String filter, List<Status> statusList) throws DotPublisherException;
228+
229+
/**
230+
* Count {@link PublishAuditStatus} matching combined text and status filters.
231+
* The text filter searches both bundle_id and bundle name (via JOIN with publishing_bundle table).
232+
*
233+
* @param filter case-insensitive partial match filter on bundle_id and bundle name, or null for no text filter
234+
* @param statusList list of statuses to filter by, or empty/null for all statuses
235+
* @return number of rows that match the filters
236+
* @throws DotPublisherException if any error occurs
237+
*/
238+
public abstract Integer countPublishAuditStatus(
239+
String filter, List<Status> statusList) throws DotPublisherException;
240+
214241
}

dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,139 @@ public List<String> getBundleIdByStatusFilterByOwner(final List<Status> statusLi
653653
return bundleIds;
654654
}
655655

656+
/**
657+
* Get {@link PublishAuditStatus} paginated with combined text and status filtering.
658+
* The text filter searches both bundle_id and bundle name via JOIN with publishing_bundle table.
659+
*
660+
* @param limit limit of rows for retrieved page
661+
* @param offset offset of rows for retrieved page
662+
* @param limitAssets max limit of assets to retrieve for each {@link PublishAuditStatus}
663+
* @param filter case-insensitive partial match filter on bundle_id and bundle name
664+
* @param statusList list of statuses to filter by, or empty/null for all statuses
665+
* @return List of {@link PublishAuditStatus}
666+
* @throws DotPublisherException if any error occurs
667+
*/
668+
@CloseDBIfOpened
669+
@Override
670+
public List<PublishAuditStatus> getPublishAuditStatus(
671+
final int limit, final int offset, final int limitAssets,
672+
final String filter, final List<Status> statusList) throws DotPublisherException {
673+
try {
674+
final DotConnect dc = buildCombinedFilterQuery(filter, statusList,
675+
SELECT_WITH_BUNDLE_JOIN, ORDER_BY_STATUS_UPDATED);
676+
dc.setStartRow(offset);
677+
dc.setMaxRows(limit);
678+
679+
return mapper.mapRows(
680+
dc.loadObjectResults().stream()
681+
.map(publishAuditStatusMap -> {
682+
final LimitedAssetResult limitedAssetResult = limitAssets(
683+
publishAuditStatusMap.get("status_pojo").toString(), limitAssets);
684+
putStatusPojoAndNumberOfAssets(publishAuditStatusMap,
685+
limitedAssetResult.newStatusPojo, limitedAssetResult.numberTotalOfAssets);
686+
return publishAuditStatusMap;
687+
})
688+
.collect(Collectors.toList())
689+
);
690+
} catch (Exception e) {
691+
Logger.debug(PublisherUtil.class, e.getMessage(), e);
692+
throw new DotPublisherException("Unable to get list of elements with error:" + e.getMessage(), e);
693+
}
694+
}
695+
696+
/**
697+
* Count {@link PublishAuditStatus} matching combined text and status filters.
698+
*
699+
* @param filter case-insensitive partial match filter on bundle_id and bundle name
700+
* @param statusList list of statuses to filter by, or empty/null for all statuses
701+
* @return number of rows that match the filters
702+
* @throws DotPublisherException if any error occurs
703+
*/
704+
@CloseDBIfOpened
705+
@Override
706+
public Integer countPublishAuditStatus(final String filter, final List<Status> statusList)
707+
throws DotPublisherException {
708+
try {
709+
final DotConnect dc = buildCombinedFilterQuery(filter, statusList,
710+
COUNT_WITH_BUNDLE_JOIN, "");
711+
return Integer.parseInt(dc.loadObjectResults().get(0).get("count").toString());
712+
} catch (Exception e) {
713+
Logger.debug(PublisherUtil.class, e.getMessage(), e);
714+
throw new DotPublisherException("Unable to count elements with error:" + e.getMessage(), e);
715+
}
716+
}
717+
718+
/**
719+
* Builds a DotConnect query with combined text and status filters.
720+
* Uses LEFT JOIN with publishing_bundle table to search by both bundle_id and bundle name.
721+
*
722+
* @param filter text filter for bundle_id and name (case-insensitive partial match)
723+
* @param statusList list of statuses to filter by
724+
* @param baseQuery the SELECT or COUNT base query
725+
* @param orderBy ORDER BY clause (empty for count queries)
726+
* @return configured DotConnect instance with parameters
727+
*/
728+
private DotConnect buildCombinedFilterQuery(final String filter, final List<Status> statusList,
729+
final String baseQuery, final String orderBy) {
730+
final String sanitizedFilter = SQLUtil.sanitizeParameter(filter);
731+
final StringBuilder sql = new StringBuilder(baseQuery);
732+
final DotConnect dc = new DotConnect();
733+
734+
// Add text filter for bundle_id and name
735+
if (UtilMethods.isSet(sanitizedFilter)) {
736+
sql.append(FILTER_BY_BUNDLE_ID_OR_NAME);
737+
}
738+
739+
// Add status filter
740+
if (UtilMethods.isSet(statusList)) {
741+
final String statusPlaceholders = statusList.stream()
742+
.map(s -> "?")
743+
.collect(Collectors.joining(","));
744+
sql.append(String.format(FILTER_BY_STATUS, statusPlaceholders));
745+
}
746+
747+
sql.append(orderBy);
748+
dc.setSQL(sql.toString());
749+
750+
// Add text filter parameters (used twice: for bundle_id and name)
751+
if (UtilMethods.isSet(sanitizedFilter)) {
752+
final String likeParam = "%" + sanitizedFilter.toLowerCase() + "%";
753+
dc.addParam(likeParam);
754+
dc.addParam(likeParam);
755+
}
756+
757+
// Add status filter parameters
758+
if (UtilMethods.isSet(statusList)) {
759+
for (final Status status : statusList) {
760+
dc.addParam(status.getCode());
761+
}
762+
}
763+
764+
return dc;
765+
}
766+
767+
// SQL constants for combined filtering with bundle JOIN
768+
private static final String SELECT_WITH_BUNDLE_JOIN =
769+
"SELECT pqa.*, pb.name as bundle_name, pb.filter_key " +
770+
"FROM publishing_queue_audit pqa " +
771+
"LEFT JOIN publishing_bundle pb ON pqa.bundle_id = pb.id " +
772+
"WHERE 1=1 ";
773+
774+
private static final String COUNT_WITH_BUNDLE_JOIN =
775+
"SELECT COUNT(*) as count " +
776+
"FROM publishing_queue_audit pqa " +
777+
"LEFT JOIN publishing_bundle pb ON pqa.bundle_id = pb.id " +
778+
"WHERE 1=1 ";
779+
780+
private static final String FILTER_BY_BUNDLE_ID_OR_NAME =
781+
"AND (LOWER(pqa.bundle_id) LIKE ? OR LOWER(pb.name) LIKE ?) ";
782+
783+
private static final String FILTER_BY_STATUS =
784+
"AND pqa.status IN (%s) ";
785+
786+
private static final String ORDER_BY_STATUS_UPDATED =
787+
"ORDER BY pqa.status_updated DESC ";
788+
656789
/**
657790
* Result from the {@link PublishAuditAPIImpl#limitAssets(String, int)} method
658791
*/
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.dotcms.rest.api.v1.publishing;
2+
3+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
4+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import org.immutables.value.Value;
7+
8+
/**
9+
* Represents a preview of an asset within a publishing bundle.
10+
* Used in the publishing jobs list to provide a quick overview of bundle contents.
11+
*
12+
* @author hassandotcms
13+
* @since Jan 2026
14+
*/
15+
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
16+
@Value.Immutable
17+
@JsonSerialize(as = AssetPreviewView.class)
18+
@JsonDeserialize(as = AssetPreviewView.class)
19+
@Schema(description = "Preview of an asset in a publishing bundle")
20+
public interface AbstractAssetPreviewView {
21+
22+
/**
23+
* The unique identifier of the asset.
24+
*
25+
* @return Asset identifier
26+
*/
27+
@Schema(
28+
description = "Asset identifier",
29+
example = "abc123-content-id",
30+
requiredMode = Schema.RequiredMode.REQUIRED
31+
)
32+
String id();
33+
34+
/**
35+
* Human-readable title of the asset.
36+
*
37+
* @return Asset title
38+
*/
39+
@Schema(
40+
description = "Human-readable asset title",
41+
example = "Homepage Content Update",
42+
requiredMode = Schema.RequiredMode.REQUIRED
43+
)
44+
String title();
45+
46+
/**
47+
* The type of asset (e.g., contentlet, template, container).
48+
*
49+
* @return Asset type
50+
*/
51+
@Schema(
52+
description = "Asset type (e.g., contentlet, template, container, folder, host)",
53+
example = "contentlet",
54+
requiredMode = Schema.RequiredMode.REQUIRED
55+
)
56+
String type();
57+
58+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.dotcms.rest.api.v1.publishing;
2+
3+
import com.dotcms.annotations.Nullable;
4+
import com.dotcms.publisher.business.PublishAuditStatus;
5+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
6+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import org.immutables.value.Value;
9+
10+
import java.time.Instant;
11+
import java.util.List;
12+
13+
/**
14+
* Represents a publishing job combining audit status and bundle metadata.
15+
* This view provides a unified representation of publishing operations including
16+
* status, bundle information, asset preview, and timing details.
17+
*
18+
* @author hassandotcms
19+
* @since Jan 2026
20+
*/
21+
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
22+
@Value.Immutable
23+
@JsonSerialize(as = PublishingJobView.class)
24+
@JsonDeserialize(as = PublishingJobView.class)
25+
@Schema(description = "Publishing job combining audit status and bundle metadata")
26+
public interface AbstractPublishingJobView {
27+
28+
/**
29+
* Unique bundle identifier.
30+
*
31+
* @return Bundle ID
32+
*/
33+
@Schema(
34+
description = "Unique bundle identifier",
35+
example = "f3d9a4b7-staging-bundle-2026-01-15",
36+
requiredMode = Schema.RequiredMode.REQUIRED
37+
)
38+
String bundleId();
39+
40+
/**
41+
* Human-readable bundle name.
42+
*
43+
* @return Bundle name or null if not set
44+
*/
45+
@Schema(
46+
description = "Human-readable bundle name",
47+
example = "bundle-testName"
48+
)
49+
@Nullable
50+
String bundleName();
51+
52+
/**
53+
* Current publishing status.
54+
*
55+
* @return Status enum value
56+
*/
57+
@Schema(
58+
description = "Current publishing status",
59+
example = "SUCCESS",
60+
requiredMode = Schema.RequiredMode.REQUIRED
61+
)
62+
PublishAuditStatus.Status status();
63+
64+
/**
65+
* Publishing filter name used for this bundle.
66+
*
67+
* @return Filter name or null if not set
68+
*/
69+
@Schema(
70+
description = "Publishing filter name used",
71+
example = "Live to Staging"
72+
)
73+
@Nullable
74+
String filterName();
75+
76+
/**
77+
* Total number of assets in the bundle.
78+
*
79+
* @return Asset count
80+
*/
81+
@Schema(
82+
description = "Total number of assets in the bundle",
83+
example = "47",
84+
requiredMode = Schema.RequiredMode.REQUIRED
85+
)
86+
int assetCount();
87+
88+
/**
89+
* Preview of first 3 assets in the bundle.
90+
*
91+
* @return List of asset previews (max 3)
92+
*/
93+
@Schema(
94+
description = "Preview of first 3 assets in the bundle",
95+
requiredMode = Schema.RequiredMode.REQUIRED
96+
)
97+
List<AssetPreviewView> assetPreview();
98+
99+
/**
100+
* Number of target environments for this bundle.
101+
*
102+
* @return Environment count
103+
*/
104+
@Schema(
105+
description = "Number of target environments",
106+
example = "3",
107+
requiredMode = Schema.RequiredMode.REQUIRED
108+
)
109+
int environmentCount();
110+
111+
/**
112+
* Bundle creation timestamp.
113+
*
114+
* @return Creation date/time
115+
*/
116+
@Schema(
117+
description = "Bundle creation timestamp in ISO 8601 format",
118+
example = "2026-01-15T10:29:55Z",
119+
requiredMode = Schema.RequiredMode.REQUIRED
120+
)
121+
Instant createDate();
122+
123+
/**
124+
* Last status update timestamp.
125+
*
126+
* @return Status update date/time or null
127+
*/
128+
@Schema(
129+
description = "Last status update timestamp in ISO 8601 format",
130+
example = "2026-01-15T10:31:22Z"
131+
)
132+
@Nullable
133+
Instant statusUpdated();
134+
135+
/**
136+
* Number of publish attempts.
137+
*
138+
* @return Number of tries
139+
*/
140+
@Schema(
141+
description = "Number of publish attempts",
142+
example = "1",
143+
requiredMode = Schema.RequiredMode.REQUIRED
144+
)
145+
int numTries();
146+
147+
}

0 commit comments

Comments
 (0)