JOLT DATA SDK GUIDE · 04

Change an Item

A Resource decides which changes an application may make. TypeScript then exposes only those methods on its Items. Application code does not manage paths, content identifiers, or concurrency bookkeeping.

Declare the allowed changes

This Task Collection allows its owner to perform the complete Item lifecycle:

src/tasks.tsts
import {
  App,
  Collection,
  Field,
  Read,
  Schema,
} from "jolt-sdk/data";

@Schema({ version: 1 })
export class Task {
  @Field.string()
  title!: string;

  @Field.boolean()
  done!: boolean;
}

export const Tasks = Collection.create(Task, {
  access: {
    read: Read.OwnIdentity,
    create: true,
    update: true,
    delete: true,
    restore: true,
  },
});

export const TaskList = App.create({
  id: "task-list.example",
  name: "Task List",
  namespace: "tasks",
  data: { tasks: Tasks },
});

Remove an operation from access when the application does not need it. For example, without delete: true, a Present Item has no delete() method.

Keep the new Item

Items are immutable snapshots. A successful mutation returns a new Item at the same stable ref; it does not change the older snapshot in place.

src/task-actions.tsts
import { Ref, State } from "jolt-sdk/data";
import { Task, TaskList } from "./mutations";

type TaskCollection = ReturnType<typeof TaskList.test>["tasks"];

export async function reviseTask(tasks: TaskCollection) {
  const created = await tasks.create({ title: "Read the guide", done: false });
  const updated = await created.update({ done: true });
  const replaced = await updated.replace({ title: "Build an app", done: true });
  const deleted = await replaced.delete();
  const restored = await deleted.restore({ title: "Build an app", done: false });

  return { created, updated, replaced, deleted, restored };
}

export async function describeTask(tasks: TaskCollection, ref: Ref<Task>) {
  const item = await tasks.get(ref);

  switch (item.state) {
    case State.Present:
      return item.value.title;
    case State.Deleted:
      return "This task was deleted";
    case State.Missing:
      return "Task not found";
    case State.Unavailable:
      return "Task is temporarily unavailable";
    default:
      throw new Error("Unknown task state");
  }
}

update() is a shallow patch. Omitted fields stay unchanged, while a supplied array or nested value replaces that whole field. Use replace() when you mean to supply the complete value.

delete() returns a Deleted Item. restore() also takes a complete value, which must match the current Schema Class. This lets an application restore old logical data into its current shape.

Read the Item state

The describeTask() example uses a normal switch over item.state:

The matching isPresent() and isDeleted() helpers are useful when an early return reads more clearly than a switch. Both forms narrow the TypeScript type.

Catch expected failures by type

Keep error handling close to the user action. Catch the specific failures the screen can explain, and rethrow anything it does not understand:

src/mutation-errors.tsts
import {
  AccessRevokedError,
  ConflictError,
  ItemUnavailableError,
  SchemaValidationError,
} from "jolt-sdk/data";

export function mutationMessage(error: unknown): string {
  if (error instanceof ConflictError) {
    return "This item changed. Read it again before retrying.";
  }
  if (error instanceof ItemUnavailableError) {
    return "Jolt cannot safely change this item right now.";
  }
  if (error instanceof AccessRevokedError) {
    return "Reconnect to Jolt and request approval again.";
  }
  if (error instanceof SchemaValidationError) {
    return `Check the ${error.field} field.`;
  }
  throw error;
}

ConflictError means the Item snapshot became stale before the mutation. Read the current Item, show it to the user when needed, and retry only if the action still makes sense. ItemUnavailableError prevents a change when current state is unknown. AccessRevokedError asks the application to reconnect and request approval again. SchemaValidationError identifies invalid application data.

Automatic conflicts are the default

Resources need no conflict configuration for normal application code. Jolt combines concurrent changes to different top-level fields, resolves concurrent changes to the same field deterministically, and lets deletion win over a concurrent update.

That automatic distributed behavior is separate from a ConflictError raised when application code tries to mutate an already stale Item snapshot. Advanced applications can override the automatic policies and expose Manual alternatives; the beginner path does not need that machinery. When a real product does, use the advanced Manual conflicts guide.

Continue learning