run-what 1.3.0

HTML-first web framework powered by Rust. No JavaScript frameworks, no build steps—just HTML.
<what>
title: "Step 7 — Database & CRUD"
active_step: 7
fetch.notes = "local:notes?sort=created_at:desc"
</what>

<h1 class="text-3xl font-bold mb-2" style="color: #111827; letter-spacing: -0.02em;">Step 7 — Database &amp; CRUD</h1>
<p class="text-gray-500 text-sm mb-8">Full create, read, update, delete — from HTML forms, no backend code.</p>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">How data fetching works</h2>
  <p class="text-gray-600 mb-4" style="line-height: 1.7;">
    Declare a <code>fetch.*</code> directive in the <code>&lt;what&gt;</code> block. The <code>local:</code> prefix queries your configured database. The result is available as a template variable.
  </p>
  <pre class="bg-gray-50 p-4 rounded text-sm font-mono" style="border: 1px solid #e5e7eb; overflow-x: auto;"><code>&lt;what&gt;
fetch.notes = "local:notes?sort=created_at:desc&amp;limit=20"
&lt;/what&gt;

&lt;loop data="#notes#" as="note"&gt;
  &lt;div&gt;#note.title#&lt;/div&gt;
&lt;/loop&gt;</code></pre>
</section>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">Create a note</h2>
  <p class="text-gray-600 mb-3" style="line-height: 1.7;">
    Use <code>action="/w-action/notes"</code> with <code>method="post"</code> to insert a new record. The form fields become the record's columns.
  </p>

  <if flash.success>
    <div class="alert alert-success mb-4">#flash.success#</div>
  </if>
  <if flash.error>
    <div class="alert alert-danger mb-4">#flash.error#</div>
  </if>

  <div class="card mb-6">
    <div class="card-body">
      <form method="post" action="/w-action/notes?w-redirect=/tutorial/7" w-validate>
        <div class="form-group">
          <label class="form-label" for="note-title">Title <span style="color: #ef4444;">*</span></label>
          <input
            type="text"
            id="note-title"
            name="title"
            class="form-control"
            placeholder="Note title..."
            w-required
            value="#old.title#"
            autocomplete="off"
            data-1p-ignore
          >
          <if errors.title>
            <div class="text-sm mt-1" style="color: #dc2626;">#errors.title#</div>
          </if>
        </div>
        <div class="form-group">
          <label class="form-label" for="note-body">Content</label>
          <textarea
            id="note-body"
            name="body"
            class="form-control"
            rows="3"
            placeholder="Write something..."
            autocomplete="off"
            data-1p-ignore
          >#old.body#</textarea>
        </div>
        <div class="flex gap-2">
          <button type="submit" class="btn btn-primary">Save note</button>
          <button type="reset" class="btn btn-outline">Cancel</button>
        </div>
      </form>
    </div>
  </div>
</section>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">Your notes</h2>
  <p class="text-gray-600 mb-3" style="line-height: 1.7;">
    Records are fetched via <code>fetch.notes = "local:notes"</code> in the <code>&lt;what&gt;</code> block at the top of this page.
  </p>

  <unless notes>
    <div class="card" style="border-style: dashed;">
      <div class="card-body text-center text-gray-400 py-8">
        No notes yet. Create one above.
      </div>
    </div>
  </unless>

  <loop data="#notes#" as="note">
    <div class="card mb-3">
      <div class="card-body">
        <div class="flex justify-between items-start">
          <div style="flex: 1;">
            <div class="font-semibold text-gray-900 mb-1">#note.title#</div>
            <if note.body>
              <div class="text-sm text-gray-600" style="line-height: 1.6;">#note.body#</div>
            </if>
          </div>
          <form method="post" action="/w-action/notes/#note.id#?w-action=delete&amp;w-redirect=/tutorial/7" style="margin-left: 1rem; flex-shrink: 0;">
            <button type="submit" class="btn btn-sm" style="background: #fef2f2; color: #dc2626; border: 1px solid #fecaca;">Delete</button>
          </form>
        </div>
      </div>
    </div>
  </loop>
</section>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">CRUD reference</h2>
  <div class="card">
    <div class="card-body" style="padding: 0;">
      <table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">
        <thead>
          <tr style="background: #f9fafb; border-bottom: 1px solid #e5e7eb;">
            <th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Action</th>
            <th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Form action</th>
            <th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Method</th>
          </tr>
        </thead>
        <tbody>
          <tr style="border-bottom: 1px solid #f3f4f6;">
            <td style="padding: 0.625rem 1rem;">Create record</td>
            <td style="padding: 0.625rem 1rem;"><code>/w-action/notes</code></td>
            <td style="padding: 0.625rem 1rem;"><code>POST</code></td>
          </tr>
          <tr style="border-bottom: 1px solid #f3f4f6;">
            <td style="padding: 0.625rem 1rem;">Update record</td>
            <td style="padding: 0.625rem 1rem;"><code>/w-action/notes/#id#</code></td>
            <td style="padding: 0.625rem 1rem;"><code>POST</code></td>
          </tr>
          <tr>
            <td style="padding: 0.625rem 1rem;">Delete record</td>
            <td style="padding: 0.625rem 1rem;"><code>/w-action/notes/#id#?w-action=delete</code></td>
            <td style="padding: 0.625rem 1rem;"><code>POST</code></td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</section>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">Where does the data go?</h2>
  <p class="text-gray-600 mb-3" style="line-height: 1.7;">
    Forms can send data to different destinations depending on the action URL:
  </p>
  <div class="card mb-4">
    <div class="card-body" style="padding: 0;">
      <table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">
        <thead>
          <tr style="background: #f9fafb; border-bottom: 1px solid #e5e7eb;">
            <th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Destination</th>
            <th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">How</th>
          </tr>
        </thead>
        <tbody>
          <tr style="border-bottom: 1px solid #f3f4f6;">
            <td style="padding: 0.625rem 1rem;">Database</td>
            <td style="padding: 0.625rem 1rem; color: #6b7280;"><code>action="/w-action/collection"</code> — persists to SQLite/D1</td>
          </tr>
          <tr style="border-bottom: 1px solid #f3f4f6;">
            <td style="padding: 0.625rem 1rem;">Session</td>
            <td style="padding: 0.625rem 1rem; color: #6b7280;"><code>w-set="session.key = $value"</code> — per-user, no form submit needed</td>
          </tr>
          <tr style="border-bottom: 1px solid #f3f4f6;">
            <td style="padding: 0.625rem 1rem;">App state</td>
            <td style="padding: 0.625rem 1rem; color: #6b7280;"><code>w-set="app.key = $value"</code> — shared across all users</td>
          </tr>
          <tr>
            <td style="padding: 0.625rem 1rem;">Wired (real-time)</td>
            <td style="padding: 0.625rem 1rem; color: #6b7280;"><code>w-set="wired.key = $value"</code> — broadcasts to all browsers via WebSocket</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</section>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">CSRF protection</h2>
  <p class="text-gray-600 mb-3" style="line-height: 1.7;">
    Every <code>&lt;form method="post"&gt;</code> automatically gets a hidden CSRF token injected by the engine. You don't need to add it manually — it's built in.
  </p>
  <pre class="bg-gray-50 p-4 rounded text-sm font-mono" style="border: 1px solid #e5e7eb; overflow-x: auto;"><code>&lt;!-- You write this: --&gt;
&lt;form method="post" action="/w-action/notes"&gt;
  &lt;input name="title"&gt;
  &lt;button type="submit"&gt;Save&lt;/button&gt;
&lt;/form&gt;

&lt;!-- The engine renders this: --&gt;
&lt;form method="post" action="/w-action/notes"&gt;
  &lt;input type="hidden" name="_csrf" value="auto-generated-token"&gt;
  &lt;input name="title"&gt;
  &lt;button type="submit"&gt;Save&lt;/button&gt;
&lt;/form&gt;</code></pre>
  <div class="alert" style="margin-top: 1rem;">
    <strong>Security:</strong> POST requests without a valid CSRF token are rejected with 403 Forbidden. This prevents cross-site request forgery attacks.
  </div>
</section>

<section class="mb-8">
  <h2 class="text-lg font-semibold mb-3" style="color: #111827;">Query options</h2>
  <pre class="bg-gray-50 p-4 rounded text-sm font-mono" style="border: 1px solid #e5e7eb; overflow-x: auto;"><code>&lt;!-- Sort, filter, search, paginate --&gt;
fetch.notes = "local:notes?sort=created_at:desc&amp;limit=10&amp;offset=0"
fetch.active = "local:items?filter=status:active"
fetch.results = "local:posts?search=hello"</code></pre>
</section>

<div class="card" style="border-color: #bbf7d0; background: #f0fdf4;">
  <div class="card-body">
    <div class="text-sm font-semibold mb-2" style="color: #14532d;">What you learned</div>
    <ul class="text-sm space-y-1" style="color: #166534; padding-left: 1.25rem; list-style: disc;">
      <li><code>fetch.name = "local:collection"</code> queries your SQLite database</li>
      <li>Fetched data is available as <code>#name#</code> and iterated with <code>&lt;loop&gt;</code></li>
      <li>Create: <code>POST /w-action/collection</code> — form fields become columns</li>
      <li>Update: <code>POST /w-action/collection/#id#</code></li>
      <li>Delete: <code>POST /w-action/collection/#id#?w-action=delete</code></li>
      <li>Query params: <code>sort</code>, <code>filter</code>, <code>search</code>, <code>limit</code>, <code>offset</code></li>
      <li>Data can go to: database (<code>/w-action/</code>), session, app state, or wired (real-time)</li>
      <li>CSRF tokens are auto-injected into all POST forms — no manual setup needed</li>
    </ul>
  </div>
</div>