Write documents from code

v0.0.1

Documents are Ash resources, so a script, a seed or an importer can write them without the admin. The documentation of Mishka Chelekom and of the other Mishka products is written this way, from HTML files. This guide creates a document, a version, a page and a note from Elixir, and copies a version into the next release.

Before you begin

  • Set the site up, so the blocks and permission names exist. See Set up documents on a site.
  • Choose the actor. A script that writes for any site runs as a master account, a user with no site, such as one holding *. A script that writes for one site runs as an account of that site, holding the permissions of Permission names, and passes tenant: site_id.
  • Pass the actor on every call. The resources' own policies and validations then decide what is written.

Create a document

A master writes with the master_create action and names the site. A site account uses create and a tenant.

elixir
alias MishkaDocument.{Note, Section, Workspace, WorkspaceVersion}

opts = [actor: master]

document =
  Workspace
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      name: "Handbook",
      slug: "handbook",
      path: "/handbook/docs",
      status: :active,
      language: "en",
      site_id: site.id
    },
    opts
  )
  |> Ash.create!()

The address is checked and stored in its normal form, and the two site pages that answer at it are written for you, as when you set an address in the admin. For a site account, call :create with actor: user, tenant: site.id and leave out site_id.

Create a version

elixir
version =
  WorkspaceVersion
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      workspace_id: document.id,
      site_id: site.id,
      version_number: "1.0.0",
      status: :stable,
      is_latest_for_version: true,
      published_at: DateTime.utc_now()
    },
    opts
  )
  |> Ash.create!()

A stable version needs published_at, and the version number must be a semantic version. Marking a version as latest unmarks the document's other versions.

Create a page and a note

A page is a Section. Set both review statuses to :approved, or readers do not see it. The slug and the content are cleaned, and code blocks are highlighted, when the page is written.

elixir
page =
  Section
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      workspace_version_id: version.id,
      site_id: site.id,
      language: "en",
      title: "Welcome",
      slug: "welcome",
      content: "<p>Start here.</p>",
      order_position: 0,
      content_review_status: :approved,
      technical_review_status: :approved
    },
    opts
  )
  |> Ash.create!()

Note
|> Ash.Changeset.for_create(
  :master_create,
  %{
    section_id: page.id,
    site_id: site.id,
    title: "Install",
    content: "<p>Add the dependency.</p>",
    order_position: 0,
    content_review_status: :approved,
    technical_review_status: :approved
  },
  opts
)
|> Ash.create!()

Give a page a parent with parent_section_id. Mark a version's front page with front_page: true, a group with group_heading: true, and a group that also has a page with group_page: true. The Chelekom importer writes each group as a page with group_heading: true, and each of its pages with that group as the parent.

Copy a version into the next release

The master_clone action copies a version with its pages, notes and tags into a new draft version of the same document.

elixir
next =
  WorkspaceVersion
  |> Ash.Changeset.for_create(
    :master_clone,
    %{version_number: "1.1.0", source_version_id: version.id},
    opts
  )
  |> Ash.create!()

next.__metadata__.cloned
#=> %{pages: 1, notes: 1, tags: 0, from: "1.0.0"}

The new version records its source. A site account uses :clone with a tenant, and needs the section area as well as the workspace area. Giving :master_create a source_version_id copies in the same way. The Mishka Chelekom importer builds each later version like that, then writes the pages of that version over the copies.

Write a script you can run twice

A create action fails when the slug or the version number is already taken. The importers find a record first, with :master_read and a filter on the slug and site, and call the update action when it exists. They give a document, a version and a page the same treatment, in that order.

Next steps