UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands

218 lines (174 loc) 9.9 kB
--- name: lunora-functions description: Authoring rules for Lunora schema and functions. Use when writing or reviewing `lunora/` code — `defineSchema`/`defineTable`, `v.*` validators, query vs mutation vs action (and `internal*`), indexes & `withIndex`, the `ctx.db` API, pagination, scheduling, and `httpAction`. --- # Lunora Functions The core authoring rules for Lunora backend code. Read this before writing or changing anything under `lunora/`. After every edit, run `lunora codegen` — it regenerates `lunora/_generated/` and typechecks your schema + functions. ## When to Use - Writing or editing schema, queries, mutations, or actions. - Reviewing `lunora/` code for correctness and idiom. - Deciding query vs mutation vs action, or public vs internal. ## When Not to Use - Setting up a new project (`lunora-quickstart`) or auth (`lunora-setup-auth`). - Diagnosing a slow query or write conflict (`lunora-performance-audit`). - Changing an existing schema with data at rest (`lunora-migration-helper`). ## Schema: `defineSchema` + `defineTable` `lunora/schema.ts` exports `defineSchema` as the default export. Every column is a `v.*` validator. Declare an index for every access pattern you query by. ```ts import { defineSchema, defineTable, v } from "@lunora/server"; export default defineSchema({ messages: defineTable({ channelId: v.id("channels"), authorId: v.id("users"), body: v.string(), createdAt: v.number(), }).index("by_channel", ["channelId", "createdAt"]), channels: defineTable({ name: v.string(), }), }); ``` - Lunora injects `_id` and `_creationTime` on every row — do **not** declare them. - `.index("name", ["a", "b"])` — columns are ordered; put equality columns first, then the range/sort column. - `.shardBy("ownerId")` partitions the table across Durable Objects by key; `.global()` replicates it to D1 for cross-region reads. Default (neither) is a single root-scoped ShardDO. They are not combined on one table — for choosing between them, see the side-by-side comparison in the `lunora-performance-audit` skill. ### Validators (`v.*`) `string`, `number`, `boolean`, `id("table")`, `null`, `any`, `bigint`, `bytes`, `literal(value)`, `array(item)`, `object({...})`, `record(key, value)`, `union(a, b, …)`, `optional(inner)`, plus the convenience types `date`, `timestamp`, and `storage` (an R2 object key). Use `v.optional(...)` for nullable fields — required is the default. ## Functions: query / mutation / action Each function declares its inputs with `.input(...)` (a `v.*` map) and ends with a terminal `.query` / `.mutation` / `.action` handler. Export them as named consts from `lunora/*.ts`; codegen surfaces them as `api.<file>.<name>`. | Kind | Reads `ctx.db` | Writes `ctx.db` | Side effects / `fetch` | Reactive | | ---------- | --------------------------------- | --------------- | ---------------------- | -------- | | `query` | yes | no | no | yes | | `mutation` | yes | yes | no | — | | `action` | no (use `runQuery`/`runMutation`) | no | yes | — | ```ts import type { Id } from "@lunora/server"; import { action, LunoraError, mutation, query, v } from "@lunora/server"; // `api` / `internal` come from codegen: // import { api, internal } from "./_generated/api"; export const listByChannel = query.input({ channelId: v.id("channels") }).query(async ({ ctx, args: { channelId } }) => ctx.db .query("messages") .withIndex("by_channel", (q) => q.eq("channelId", channelId)) .collect(), ); export const send = mutation .input({ channelId: v.id("channels"), body: v.string() }) .mutation(async ({ ctx, args: { channelId, body } }): Promise<Id<"messages">> => { if (!ctx.auth.userId) { throw new LunoraError("UNAUTHORIZED", "not signed in"); } return ctx.db.insert("messages", { channelId, authorId: ctx.auth.userId as Id<"users">, body, createdAt: Date.now(), }); }); export const notifySlack = action.input({ messageId: v.id("messages") }).action(async ({ ctx, args: { messageId } }) => { const message = await ctx.runQuery(api.messages.getById, { messageId }); await fetch(SLACK_WEBHOOK, { method: "POST", body: JSON.stringify(message) }); }); ``` - **Pick the right kind.** Reactive read`query`. Transactional write`mutation`. External I/O (`fetch`, third-party SDKs, calling other functions) → `action`. An action has no `ctx.db`; it reaches data via `ctx.runQuery` / `ctx.runMutation`. - **`internal*` variants** (`internalQuery`, `internalMutation`, `internalAction`) are not exposed to clients — use them for server-only logic called from actions, crons, or other functions. - **Throw `LunoraError`** (`import { LunoraError } from "@lunora/server"`) with a code + message for expected failures; it serializes cleanly to the client. ## The `ctx.db` API Reads: ```ts await ctx.db.get(id); // one row by id (or null) ctx.db.query("t").withIndex("by_x", (q) => q.eq("x", v)); // indexed query .collect(); // all matching rows .first(); // first row or null .unique(); // exactly one (throws if 0 or >1) .take(n); // first n rows .order("asc" | "desc") // sort by the index range .paginate(opts); // cursor page (pair with usePaginatedQuery) ``` Writes (mutations only): ```ts await ctx.db.insert("t", { ...fields }); // returns the new Id await ctx.db.patch(id, { field: next }); // shallow-merge update await ctx.db.replace(id, { ...allFields }); // full overwrite await ctx.db.delete(id); ``` **Prefer `withIndex` over `.filter`.** A `.filter(...)` with no covering index scans the whole table — `@lunora/advisor` flags it as `filter-without-index`. Declare the index and constrain with `.withIndex`. ## Other `ctx` capabilities Always available: - `ctx.auth` — the resolved session (`ctx.auth.userId`). - `ctx.scheduler``runAfter` / `runAt` for deferred work. - `ctx.secrets` — Cloudflare Secrets Store. - `ctx.span` / `ctx.trace` — the current span and a scoped tracing helper for wide events. Added by their package when wired. **A dependency in `package.json` is not enough** — codegen scans the `lunora/` source set and flips a capability on only when a file there imports the `@lunora/*` package or reads its `ctx.*` helper. So write the call first, then run `lunora codegen` to surface the typed context: | `ctx.*` | Package | | ----------------------------------------------------------------------------------------- | ----------------------------- | | `ctx.storage` | `@lunora/storage` (R2) | | `ctx.ai` | `@lunora/ai` (Workers AI) | | `ctx.flags` | `@lunora/flags` (OpenFeature) | | `ctx.queues.<name>` | `@lunora/queue` | | `ctx.workflows` / `ctx.runStep` | `@lunora/workflow` | | `ctx.containers` | `@lunora/container` | | `ctx.browser` (action-only) | `@lunora/browser` | | `ctx.sql` (action-only) | `@lunora/hyperdrive` | | `ctx.kv` / `ctx.images` / `ctx.analytics` / `ctx.pipelines` / `ctx.vectors` / `ctx.r2sql` | `@lunora/bindings` subpaths | Two exceptions to the usage scan, and one extra requirement: - **`ctx.flags` gates on a declaration file**, not on usage — codegen wires it only when `lunora/flags.ts` exists (`vis generate lunora-flags` creates it). `ctx.notify` / `ctx.push` work the same way via `lunora/notify.ts`. - **`ctx.sql` also needs the real resource.** Codegen types the field, but the connection needs a `HYPERDRIVE` binding (`wrangler hyperdrive create`) and an explicit `createHyperdrive(ctx.env.HYPERDRIVE)` + driver adapter in the action — see `lunora-setup-hyperdrive`. Bindings codegen can provision on its own (e.g. `BROWSER` for `ctx.browser`) need no manual wrangler step. `ctx.browser` and `ctx.sql` are **action-only** by design — they are non-deterministic and would break query reactivity and mutation replay. ## HTTP endpoints For webhooks or non-RPC HTTP, use `httpRouter` / `httpRoute` + `httpAction`: ```ts import { httpAction, httpRouter } from "@lunora/server"; export default httpRouter({ "/webhooks/stripe": httpAction(async (ctx, request) => { const event = await request.json(); await ctx.runMutation(internal.billing.record, { event }); return new Response("ok"); }), }); ``` ## Checklist - [ ] Schema columns are `v.*` validators; `_id`/`_creationTime` not declared. - [ ] An index exists for every access pattern; queries use `withIndex`, not `.filter`. - [ ] Right function kind: `query` (reactive read) / `mutation` (write) / `action` (side effects via `runQuery`/`runMutation`). - [ ] Server-only logic uses `internal*`; expected failures throw `LunoraError`. - [ ] `ctx.db` writes only inside mutations; ids typed with `Id<"table">`. - [ ] Ran `lunora codegen`; typecheck is clean.