Skip to main content

Create, Update, and Delete Content

Content mutations enable full programmatic content lifecycle management through the GCA, allowing you to create, update, archive, publish, and delete content items via GraphQL operations. By configuring content types as mutable (via mutableEntryClass) and selecting which write actions to expose (via contentActionType), you get mutations that respect Brightspot's workflow, permissions, and validation rules.

Mutations handle complex scenarios including uploading and managing images and files (StorageItem fields), setting references to other content items, saving embedded objects, and executing multiple writes within a transaction. The mutation input shapes mirror your Java content type structure, and responses return the same content model types used by Get and Query.

Configuration

Two settings work together: mutableEntryClass marks which types can be written, and contentActionType selects which write operations appear in the schema:

1
import java.util.Set;
2
3
import com.psddev.dari.db.Recordable;
4
import com.psddev.dari.db.Singleton;
5
import com.psddev.graphql.gca.GCAEndpoint;
6
import com.psddev.graphql.gca.GCASchemaSettings;
7
import com.psddev.graphql.gca.schema.types.db.write.archive.ArchiveActionDefinition;
8
import com.psddev.graphql.gca.schema.types.db.write.delete.DeleteActionDefinition;
9
import com.psddev.graphql.gca.schema.types.db.write.save.SaveActionDefinition;
10
11
@Recordable.DisplayName("Content API")
12
public class ContentApiEndpoint extends GCAEndpoint implements Singleton {
13
14
@Override
15
public Set<String> getPaths() {
16
return Set.of("/content-api");
17
}
18
19
@Override
20
protected GCASchemaSettings getSchemaSettings() {
21
return GCASchemaSettings.newBuilder()
22
.readonlyEntryClass(Author.class)
23
.mutableEntryClass(Article.class)
24
.contentActionType(SaveActionDefinition.class)
25
.contentActionType(ArchiveActionDefinition.class)
26
.contentActionType(DeleteActionDefinition.class)
27
.build();
28
}
29
}
tip

Use contentActionTypesAll() to automatically include every available action, including any custom WriteOperationDefinition implementations in your project.

Anatomy of a mutation

All content writes live under the root mutation's Content field:

  • Content/Action/<Action>/Record(context, args) — perform a single write action.
  • Content/Transaction(context, writes) — perform multiple writes atomically. See Transactions.

Every action takes two arguments:

  • context (ContentActionContextInput) — cross-cutting execution options, most notably dryRun (see Dry runs).
  • args — the action-specific input. For all actions except Save, this is just with, the record lookup. The lookup input works exactly like Get's: by _id, by URL, or by a unique field.

Available actions

ActionArgumentsResult fieldsDescription
Savewith (optional), main, embeds, urlsstate, saveInfoCreates new or updates existing content.
PublishwithstateMoves content to the Published state.
Archivewitharchived, stateSoft delete—flags the record as archived, hiding it from default queries.
Restorewith, restoreTo: DRAFT | LIVErestored, stateRestores archived content back to draft or live.
Deletewithdeleted, statePermanently deletes the record from the database.
Workflowwith, transition, commentlog, stateMoves content through a workflow transition, recording a WorkflowLog.

Every result exposes state—the record after the action—so you can confirm exactly what was written.

Saving content

The Save action both creates and updates. Its main argument is a diff: an input object named <Type>Diff containing only the fields you want to change. Omit with to create new content; provide it to update an existing record:

1
mutation SaveArticle($context: ContentActionContextInput = {}, $args: RecordSaveActionInput!) {
2
Content {
3
Action {
4
Save {
5
Record(context: $context, args: $args) {
6
state {
7
__typename
8
_id
9
... on Article {
10
headline
11
tags
12
author {
13
_id
14
}
15
}
16
}
17
}
18
}
19
}
20
}
21
}
22

Fields not present in the diff are untouched, which makes Save safe for concurrent, partial updates—two clients updating different fields won't clobber each other.

Setting references

Reference fields (like Article.author) accept the same one-of lookup input used everywhere else: {_id: ...}, a URL, or a unique field value. The referenced record must already exist—or be created in the same request and linked by named reference.

Linking embedded and new records

Embedded records (@Embedded types stored inside their parent) are created through the Save action's embeds argument—a list of diffs alongside main. The parent field links to entries in that list in one of two ways:

  • {_index: N} — by position in the embeds array.
  • {_ref: "name"} — by a _name you assign inside the embedded diff.
1
{
2
"context": {},
3
"args": {
4
"main": {
5
"AuthorDiff": {
6
"name": "Jane Doe",
7
"email": "jane@example.com",
8
"addresses": [
9
// !tooltip[/_index/] Link to the embedded object by its position in the `embeds` array below.
10
{"_index": 0},
11
// !tooltip[/_ref/] Link to the embedded object by the `_name` you assigned it in the `embeds` array.
12
{"_ref": "office"}
13
]
14
}
15
},
16
"embeds": [
17
{
18
"AddressDiff": {"street": "21 Jump Street", "city": "Reston"}
19
},
20
{
21
// !tooltip[/_name/] An arbitrary name that other diffs in this mutation can reference via `_ref`.
22
"AddressDiff": {"_name": "office", "street": "22 Jump Street", "city": "Reston"}
23
}
24
]
25
}
26
}
27

To update an existing embedded record, reference it by its {_id: ...} instead.

Uploading files and images

StorageItem fields accept a structured input with a one-of data field plus optional metadata:

InputDescription
data.fileA multipart file upload—binary data sent with the request. Stored in your configured storage backend with metadata (dimensions, EXIF/IPTC) extracted automatically.
data.urlA URL the system downloads, stores, and extracts metadata from.
data.urlRefA URL stored as-is—no download, no metadata extraction.
metadataMerge (put) or replace custom metadata entries on the item.
1
{
2
"context": {},
3
"args": {
4
"with": {
5
"_id": "0000019b-a37e-ddc0-af9f-b37edded0000"
6
},
7
"main": {
8
"ArticleDiff": {
9
"leadImage": {
10
"data": {
11
// !tooltip[/url/] The file at this URL is downloaded, stored in your configured storage backend, and its metadata (dimensions, EXIF, etc.) is extracted. Use `urlRef` instead to reference the URL as-is without downloading, or `file` with a multipart upload to send binary data directly.
12
"url": "https://example.com/images/lead.jpg"
13
},
14
// !tooltip[/metadata/] Optionally merge custom metadata into the stored item.
15
"metadata": {
16
"put": [{
17
"key": "credit",
18
"value": "Jane Doe"
19
}]
20
}
21
}
22
}
23
}
24
}
25
}
26

Detecting conflicting writes

Save accepts a readTimestamp argument: pass the lastUpdate value from the Get query you used to fetch the record. If any field you're modifying changed after that time, the save fails instead of silently overwriting someone else's work—your application can then re-fetch and retry.

Archive vs. Delete

These are commonly confused:

  • Archive is a save that flags the record (the CMS "Archive" / trash behavior). The record remains in the database, disappears from default queries, and can be brought back with Restore.
  • Delete permanently removes the record. There is no undo.
1
mutation ArchiveArticle($context: ContentActionContextInput = {}, $args: RecordArchiveActionInput!) {
2
Content {
3
Action {
4
Archive {
5
Record(context: $context, args: $args) {
6
archived
7
}
8
}
9
}
10
}
11
}
12

Dry runs

Pass context: {dryRun: true} to execute the full mutation—validation, hooks, computed state—without persisting anything. The response includes the would-be result plus a dryRuns entry in the response extensions identifying which operations were rolled back:

1
"extensions": {
2
"dryRuns": [{ "path": "/Content/Action/Save/Record" }]
3
}

Dry runs are invaluable for validating editorial input in custom tools before committing, and for testing migrations safely.

Transactions

Content/Transaction executes a list of write operations inside a single database transaction—either all succeed and commit, or the transaction is rolled back. Each entry in the writes list is a one-of input selecting an action with the same args shape as the standalone version, and the result exposes a corresponding list of per-write results.

Within a transaction:

  • All writes observe the same timestamp (database.now() is pinned for the duration).
  • Named references (_ref) are shared across the whole request, so one write can reference content created by another.
  • dryRun applies to the entire transaction.

Attribution, sites, and permissions

By default, mutations are unattributed. Two supplier settings integrate writes with Brightspot's editorial model:

  • toolUserSupplier(...) — supplies the ToolUser to attribute saves to (publish user, update user). With attribution in place, published saves also generate History revisions, visible via Get's Revision field.
  • siteSupplier(...) — supplies the current Site. New content is owned by that site, and writes to content not accessible from that site are rejected.

Mutations also run Brightspot's standard validation: @Required fields, custom beforeSave logic, and write hooks all apply. Validation failures surface as GraphQL errors; enable verboseClientErrors() during development for more detail (it may expose sensitive data, so leave it off in production).

Next steps

Was this page helpful?

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.