Skip to content

Persistence ​

Records are saved, updated, and destroyed with save, create, update, and destroy.

Saving records ​

Save a record with save. It validates the record, runs its callbacks, and resolves to true or false:

ts
import { User } from "./models";

const user = User.build({ email: "ada@example.com" });
if (await user.save()) {
  // saved: user.isPersisted() is true
} else {
  console.log(user.errors.fullMessages()); // ["Email has already been taken"]
}

const created = await User.create({ email: "grace@example.com" }); // saved, or not
await created.update({ isActive: false }); // true or false

A new record is inserted, and a persisted one updates only the attributes that changed. save resolves to false for one of two reasons:

  • Validation failed. The reasons are in record.errors.
  • A callback aborted with abort(). The reason is in record.abortReason.

create(attrs) builds and saves a record, and resolves to it whether it saved or not. Check isPersisted() or errors. update(attrs) assigns, then saves.

Throwing on failure ​

When a failure is exceptional, use the OrThrow version of each method:

ts
import { RecordInvalid } from "@forestfuture/d1-record";
import { User } from "./models";

try {
  const grace = await User.createOrThrow({ email: "grace@example.com" });
  await grace.updateOrThrow({ isActive: true });
} catch (error) {
  if (error instanceof RecordInvalid) {
    return Response.json({ errors: error.record.errors.fullMessages() }, { status: 422 });
  }
  throw error; // anything else is a real failure
}

Instead of returning false, they throw:

  • RecordInvalid when validation failed, with the record on error.record.
  • RecordNotSaved when a callback aborted a save, with the abort’s reason as its message.
  • RecordNotDestroyed from destroyOrThrow(), when a callback aborted the destroy or a restrictWithError association refused it.

Compared with Rails

The OrThrow methods are Rails’ save!, create!, update!, and destroy!.

What each method returns ​

MethodResolves toThrows
save() / update(attrs)true / falseconstraint errors, RecordDestroyed
saveOrThrow() / updateOrThrow(attrs)truesame as above, plus RecordInvalid, RecordNotSaved
create(attrs)record, persisted or notconstraint errors
createOrThrow(attrs)persisted recordsame as above, plus RecordInvalid, RecordNotSaved
destroy()destroyed record, or false on abortconstraint errors, RecordDestroyed
destroyOrThrow()destroyed recordsame as above, plus RecordNotDestroyed
find(id)recordRecordNotFound
findBy(conds)record or nullnone
findByOrThrow(conds)recordRecordNotFound

false only ever means validation failed or a callback aborted. Every method throws for anything else:

  • Constraint errors, such as RecordNotUnique, since the database refused the write. See Errors.
  • An error thrown by a callback, other than abort(), even from an after-callback once the record is saved.
  • D1’s own errors.

Destroying records ​

Destroy a record with destroy:

ts
const destroyed = await created.destroy(); // the record, or false if a callback aborted
if (destroyed) {
  destroyed.isDestroyed(); // true: readable, but can't be changed or saved
}

It runs the destroy callbacks and the associations’ dependent options, then deletes the row. A destroyed record’s attributes stay readable, but setting one, or calling save, update, destroy, or reload, throws RecordDestroyed.

TIP

If another request already deleted the row, save and destroy still succeed.

Checking a record’s state ​

StateisNewRecord()isPersisted()isDestroyed()
New (from build(), or after a failed create)truefalsefalse
Persisted (loaded, or after a successful save)falsetruefalse
Destroyed (after a successful destroy())falsefalsetrue

Reloading records ​

Load a record’s row again with reload, discarding unsaved changes and loaded associations:

ts
import { User } from "./models";

const fresh = await User.find(user.id);
fresh.email = "changed@example.com";
await fresh.reload(); // back to what D1 holds

It throws RecordNotFound if the row is gone, or the record was never saved.

Writing many rows ​

To insert, update, or delete many rows at once, without validations and callbacks, use insertAll, upsertAll, updateAll, and deleteAll. See Bulk writes and batches.