Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1fcf41a
feat(publishing): add form and view DTOs for bundle asset management …
hassandotcms Mar 17, 2026
3fed4ad
test(publishing): add integration tests for BundleManagementResource …
hassandotcms Mar 17, 2026
1d58ce3
docs(publishing): update OpenAPI spec for bundle asset endpoints #35023
hassandotcms Mar 17, 2026
95162c9
Merge branch 'main' of https://github.com/dotCMS/core into 35023-task…
hassandotcms Mar 30, 2026
44bf49d
fix(openapi): update Publishing tag description to reflect push publi…
hassandotcms Mar 30, 2026
bc6a502
fix(publishing): pre-filter duplicate assets to prevent batch failure…
hassandotcms Mar 30, 2026
6ff5d10
fix(publishing): map DotPublisherException to 500 and clarify total f…
hassandotcms Mar 30, 2026
a72da5b
fix(publishing): add in-progress guard, null-safe casts, and input va…
hassandotcms Apr 8, 2026
b462569
Merge branch 'main' of https://github.com/dotCMS/core into 35023-task…
hassandotcms Apr 8, 2026
0707288
Merge branch 'main' of https://github.com/dotCMS/core into 35023-task…
hassandotcms Apr 8, 2026
dfe2d02
fix(publishing): return 404 when bundleId not found and bundleName ab…
hassandotcms Apr 8, 2026
316ec9d
docs(openapi): add 404 response for POST /v1/bundles/assets
hassandotcms Apr 9, 2026
5d59790
fix(publishing): filter blank assetIds in DELETE, add missing tests f…
hassandotcms Apr 9, 2026
6f44b39
Merge branch 'main' of https://github.com/dotCMS/core into 35023-task…
hassandotcms Apr 9, 2026
daf840e
Merge branch 'main' of https://github.com/dotCMS/core into 35023-task…
hassandotcms Apr 13, 2026
77f9fc8
fix(publishing): prevent NPE in bundle name filter when DB row has nu…
hassandotcms Apr 13, 2026
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
@@ -0,0 +1,89 @@
package com.dotcms.rest.api.v1.publishing;

import com.dotcms.annotations.Nullable;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

import java.util.List;

/**
* Result of adding assets to a bundle.
* Contains the bundle details and per-asset error information.
*
* @author hassandotcms
* @since Mar 2026
*/
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = AddAssetsToBundleView.class)
@JsonDeserialize(as = AddAssetsToBundleView.class)
@Schema(description = "Result of adding assets to a bundle")
public interface AbstractAddAssetsToBundleView {

/**
* The bundle identifier used (new or existing).
*
* @return Bundle ID
*/
@Schema(
description = "Bundle identifier (new or existing)",
example = "550e8400-e29b-41d4-a716-446655440000",
requiredMode = Schema.RequiredMode.REQUIRED
)
String bundleId();

/**
* The bundle name.
*
* @return Bundle name or null
*/
@Schema(
description = "Name of the bundle",
example = "My Content Bundle"
)
@Nullable
String bundleName();

/**
* Whether a new bundle was created by this call.
*
* @return true if a new bundle was created
*/
@Schema(
description = "Whether a new bundle was created by this call",
example = "false",
requiredMode = Schema.RequiredMode.REQUIRED
)
boolean created();

/**
* Total number of non-duplicate assets processed.
* Assets already in the bundle are silently skipped and not included in this count.
* Subtract {@code errors().size()} to get the count of successfully added assets.
*
* @return Total non-duplicate assets processed
*/
@Schema(
description = "Total non-duplicate assets processed (subtract errors count for successful adds). "
+ "Assets already in the bundle are skipped and excluded from this count.",
example = "5",
requiredMode = Schema.RequiredMode.REQUIRED
)
int total();

/**
* Per-asset error messages as human-readable strings (e.g., permission denied).
* Empty list means all assets were added successfully.
*
* @return List of error messages
*/
@Schema(
description = "Per-asset error messages as human-readable strings. " +
"Empty list = all assets added successfully",
requiredMode = Schema.RequiredMode.REQUIRED
)
List<String> errors();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.dotcms.rest.api.v1.publishing;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

/**
* Result of removing a single asset from a bundle.
* Contains success/failure status and a human-readable message.
*
* @author hassandotcms
* @since Mar 2026
*/
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = RemoveAssetResultView.class)
@JsonDeserialize(as = RemoveAssetResultView.class)
@Schema(description = "Result of removing a single asset from a bundle")
public interface AbstractRemoveAssetResultView {

/**
* The asset identifier that was processed.
*
* @return Asset ID
*/
@Schema(
description = "The asset identifier that was processed",
example = "550e8400-e29b-41d4-a716-446655440000",
requiredMode = Schema.RequiredMode.REQUIRED
)
String assetId();

/**
* Whether the asset was successfully removed.
*
* @return true if removal was successful
*/
@Schema(
description = "Whether the asset was successfully removed from the bundle",
example = "true",
requiredMode = Schema.RequiredMode.REQUIRED
)
boolean success();

/**
* Human-readable result message.
*
* @return Result message
*/
@Schema(
description = "Human-readable result message",
example = "Asset removed from bundle",
requiredMode = Schema.RequiredMode.REQUIRED
)
String message();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.dotcms.rest.api.v1.publishing;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

import java.util.List;

/**
* Form for removing assets from a publishing bundle.
* Contains the list of asset identifiers to remove.
*
* @author hassandotcms
* @since Mar 2026
*/
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = RemoveAssetsFromBundleForm.class)
@JsonDeserialize(as = RemoveAssetsFromBundleForm.class)
@Schema(description = "Request body for removing assets from a bundle")
public interface AbstractRemoveAssetsFromBundleForm {

/**
* List of asset identifiers to remove from the bundle.
*
* @return List of asset IDs
*/
@Schema(
description = "List of asset identifiers to remove from the bundle",
example = "[\"asset-123\", \"asset-456\"]",
requiredMode = Schema.RequiredMode.REQUIRED
)
List<String> assetIds();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.dotcms.rest.api.v1.publishing;

import com.dotcms.rest.api.Validated;
import com.dotcms.rest.exception.BadRequestException;
import com.dotmarketing.util.UtilMethods;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;
import java.util.stream.Collectors;

/**
* Form for adding assets to a publishing bundle.
* Supports referencing an existing bundle by ID or name, or auto-creating a new one.
*
* @author hassandotcms
* @since Mar 2026
*/
@Schema(description = "Form for adding assets to a bundle")
public class AddAssetsToBundleForm extends Validated {

@Schema(
description = "Optional bundle ID. If provided and found, assets are added to this bundle. " +
"If not found, falls through to bundleName lookup, then auto-creation — does NOT return 404",
example = "550e8400-e29b-41d4-a716-446655440000"
)
private String bundleId;

@Schema(
description = "Optional bundle name. Used as fallback when bundleId is not provided or not found. " +
"Searches unsent bundles by name (case-insensitive). If no match, a new bundle is created with this name",
example = "My Content Bundle"
)
private String bundleName;

@Schema(
description = "List of asset identifiers to add to the bundle",
example = "[\"asset-123\", \"asset-456\"]",
requiredMode = Schema.RequiredMode.REQUIRED
)
private List<String> assetIds;

/**
* Default constructor for Jackson deserialization.
*/
public AddAssetsToBundleForm() {
}

/**
* Constructor with all fields for testing.
*
* @param bundleId Optional existing bundle ID
* @param bundleName Optional bundle name for lookup or creation
* @param assetIds Asset identifiers to add
*/
@JsonCreator
public AddAssetsToBundleForm(
@JsonProperty("bundleId") final String bundleId,
@JsonProperty("bundleName") final String bundleName,
@JsonProperty("assetIds") final List<String> assetIds) {
this.bundleId = bundleId;
this.bundleName = bundleName;
this.assetIds = assetIds;
}

@Override
public void checkValid() {
super.checkValid();

if (!UtilMethods.isSet(bundleId) && !UtilMethods.isSet(bundleName)) {
throw new BadRequestException(
"At least one of bundleId or bundleName is required");
}

if (UtilMethods.isSet(assetIds)) {
assetIds = assetIds.stream()
.filter(UtilMethods::isSet)
.collect(Collectors.toList());
}

if (!UtilMethods.isSet(assetIds) || assetIds.isEmpty()) {
throw new BadRequestException("assetIds must not be null or empty");
}
}

public String getBundleId() {
return bundleId;
}

public void setBundleId(final String bundleId) {
this.bundleId = bundleId;
}

public String getBundleName() {
return bundleName;
}

public void setBundleName(final String bundleName) {
this.bundleName = bundleName;
}

public List<String> getAssetIds() {
return assetIds;
}

public void setAssetIds(final List<String> assetIds) {
this.assetIds = assetIds;
}

}
Loading
Loading