writeonce/docs/examples/blog/README.md

175 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `blog` — a sample writeonce app
A complete blogging website in **~200 lines of `.wo`** that creates a database, exposes REST + live-subscription endpoints, renders HTML pages, enforces row-level policies, and emits typed client SDKs.
> This project is a **docs artifact** — it illustrates the shape of a real `wo init`'d project. The `wo` toolchain referenced here is the one specified in [`../../runtime/wo-language.md`](../../runtime/wo-language.md); the engine is at prototype stage in [`../../../prototypes/wo-db/`](../../../prototypes/wo-db/).
## What it does
| Thing | How |
| --- | --- |
| Persists articles, authors, tags, comments | `type` declarations compiled to relational rows + embedded documents + graph edges |
| Serves 24 REST endpoints (CRUD + subscribe × 4 types) | `service rest` blocks on each type |
| Serves 4 web pages (list, detail, tag, admin) | `##ui` screens + route table in `app.wo` |
| Pushes live updates on every commit | `live: true` on screens + `LIVE` queries under the hood |
| Enforces "drafts hidden from anonymous readers" | `policy read anyone when published == true` |
| Bumps `published_at` automatically | `on update` trigger inside the transaction |
| Generates a typed Go client | `wo gen sdk --lang go` |
## Project layout
```
blog/
├── wo.toml # project manifest (like go.mod)
├── app.wo # routes, theme, startup hooks
├── types/
│ ├── author.wo # Author type + per-type service/policy
│ ├── article.wo # Article — all three paradigms in one type
│ ├── tag.wo # Tag taxonomy
│ └── comment.wo # Reader comments
├── ui/
│ ├── article_list.wo # home page list view (live)
│ └── article_detail.wo # per-article page with comments + related
└── tests/
└── article_test.wo # `wo test` picks this up
```
No `main.wo` is needed — a pure type+service app auto-generates its entry point. Add `main.wo` if you need CLI args, background workers, or custom startup logic beyond the `on startup` hook in `app.wo`.
## Run it
```bash
$ cd docs/examples/blog
$ wo run
[wo] parsing: 7 files, 4 types, 2 ui screens
[wo] compiling schema: 4 sql tables, 1 doc collection, 3 graph edge types
[wo] starting runtime (engine: in-memory, data_dir: ./data)
[wo] on startup: seed_admin() — inserted admin@example.com
[wo] HTTP listening on :8080
GET /api/articles list
GET /api/articles/:id get
POST /api/articles create
PATCH /api/articles/:id update
DELETE /api/articles/:id delete
WS /api/articles/live subscribe
GET /api/authors list
GET /api/authors/me me
WS /api/authors/live subscribe
GET /api/tags list
GET /api/comments list
POST /api/comments create
WS /api/comments/live subscribe
... (and the rest)
GET / ui.article-list
GET /article/:slug ui.article-detail
GET /tag/:slug ui.article-list (filtered)
GET /admin ui.article-list (role: Admin)
```
## Exercise the REST API
```bash
# Create an author (requires admin session — see auth docs; stub'd here for brevity)
$ curl -X POST localhost:8080/api/authors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"email":"alice@example.com","handle":"alice","display":"Alice","role":"Author"}'
{"id":2,"email":"alice@example.com","handle":"alice",...}
# Create an article as that author
$ curl -X POST localhost:8080/api/articles \
-H "Authorization: Bearer $ALICE_TOKEN" \
-d '{
"slug": "hello",
"title": "Hello, writeonce",
"author": 2,
"meta": {"excerpt":"First post","body_md":"# Hi\n\nHello."},
"published": true
}'
{"id":1,"slug":"hello","title":"Hello, writeonce","published_at":"2026-04-17T12:00:00Z",...}
# List published articles (public — no token)
$ curl localhost:8080/api/articles
[{"id":1,"slug":"hello","title":"Hello, writeonce",...}]
# Filter by tag (via the query layer)
$ curl 'localhost:8080/api/articles?tags.slug=rust'
[...]
```
## Subscribe to live updates
```bash
$ websocat ws://localhost:8080/api/articles/live?published=eq.true
{"kind":"snapshot","rows":[{"id":1,"slug":"hello",...}]}
# Now in another terminal, update article 1. The open socket receives:
{"kind":"update","id":1,"old":{"title":"Hello, writeonce"},"new":{"title":"Hello!"}}
```
No polling. The subscription predicate was registered at connect time; the engine's commit path emits the delta directly.
## Generate a Go client
```bash
$ wo gen sdk --lang go --out ./client
[wo] reading types from ./types/
[wo] writing ./client/sdk.go (4 types, 16 endpoints, 4 subscriptions)
```
Use it:
```go
import "github.com/you/blog/client"
c, _ := client.Connect(ctx, "wo://localhost:8080", client.WithToken(token))
// Typed query
articles, _ := c.Articles.List(ctx, client.Where{Published: ptr(true)})
// Typed subscription — deltas arrive on a channel
sub, _ := c.Articles.Subscribe(ctx, client.Where{Published: ptr(true)})
for d := range sub.C {
switch d.Kind {
case client.Insert:
fmt.Printf("new article: %s\n", d.Row.Title)
case client.Update:
fmt.Printf("updated: %s\n", d.Row.Slug)
}
}
```
## Run the tests
```bash
$ wo test
=== tests/article_test.wo ===
create and fetch by slug OK (3ms)
policy blocks public read of unpublished drafts OK (4ms)
graph traversal: related articles OK (7ms)
live subscription receives delta on commit OK (12ms)
PASS 4/4 tests, 0 failures (26ms)
```
Each `test` block runs against an isolated engine snapshot that's rolled back at the end — no setup/teardown code needed.
## Build a production binary
```bash
$ wo build --target linux-amd64 --out bin/blog
[wo] static binary: bin/blog (14 MB, database + HTTP + subscription engine embedded)
$ ./bin/blog
[wo] HTTP listening on :8080
```
One binary, no dependencies. Copy it to a server, run it, done. The database file lives in `./data/` relative to the binary; the WAL ensures crash safety ([Phase 3](../../runtime/database/03-inmemory-engine.md)).
## What to read next
- [`../../runtime/wo-language.md`](../../runtime/wo-language.md) — the user-facing language overview this project builds on
- [`../../runtime/database/02-wo-language.md`](../../runtime/database/02-wo-language.md) — the two-layer language spec (schema + query layers)
- [`../../runtime/database/06-lowcode-fullstack.md`](../../runtime/database/06-lowcode-fullstack.md) — the `##ui`/`##policy`/`##service`/`##app` block spec
- [`../../../prototypes/wo-db/`](../../../prototypes/wo-db/) — the C++ prototype that runs the query-layer subset today