@lunora/cli 1.0.0-alpha.48 → 1.0.0-alpha.481

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE.md +33 -0
  2. package/README.md +3 -1
  3. package/dist/bin.mjs +3 -10
  4. package/dist/cli.d.mts +31 -0
  5. package/dist/cli.d.ts +31 -0
  6. package/dist/cli.mjs +1 -0
  7. package/dist/index.d.mts +1168 -416
  8. package/dist/index.d.ts +1168 -416
  9. package/dist/index.mjs +1 -19
  10. package/dist/packem_chunks/cli.mjs +2 -0
  11. package/dist/packem_chunks/dispatch.mjs +1 -0
  12. package/dist/packem_chunks/handler.mjs +61 -157
  13. package/dist/packem_chunks/handler10.mjs +2 -16
  14. package/dist/packem_chunks/handler11.mjs +1 -22
  15. package/dist/packem_chunks/handler12.mjs +1 -192
  16. package/dist/packem_chunks/handler13.mjs +1 -131
  17. package/dist/packem_chunks/handler14.mjs +3 -65
  18. package/dist/packem_chunks/handler15.mjs +1 -58
  19. package/dist/packem_chunks/handler16.mjs +1 -80
  20. package/dist/packem_chunks/handler17.mjs +1 -43
  21. package/dist/packem_chunks/handler18.mjs +1 -105
  22. package/dist/packem_chunks/handler19.mjs +1 -170
  23. package/dist/packem_chunks/handler2.mjs +1 -114
  24. package/dist/packem_chunks/handler20.mjs +1 -94
  25. package/dist/packem_chunks/handler21.mjs +7 -94
  26. package/dist/packem_chunks/handler22.mjs +1 -0
  27. package/dist/packem_chunks/handler23.mjs +3 -0
  28. package/dist/packem_chunks/handler24.mjs +3 -0
  29. package/dist/packem_chunks/handler25.mjs +2 -0
  30. package/dist/packem_chunks/handler26.mjs +1 -0
  31. package/dist/packem_chunks/handler27.mjs +2 -0
  32. package/dist/packem_chunks/handler28.mjs +99 -0
  33. package/dist/packem_chunks/handler29.mjs +3 -0
  34. package/dist/packem_chunks/handler3.mjs +1 -204
  35. package/dist/packem_chunks/handler30.mjs +15 -0
  36. package/dist/packem_chunks/handler31.mjs +1 -0
  37. package/dist/packem_chunks/handler32.mjs +1 -0
  38. package/dist/packem_chunks/handler4.mjs +1 -33
  39. package/dist/packem_chunks/handler5.mjs +1 -49
  40. package/dist/packem_chunks/handler6.mjs +3 -91
  41. package/dist/packem_chunks/handler7.mjs +1 -42
  42. package/dist/packem_chunks/handler8.mjs +1 -174
  43. package/dist/packem_chunks/handler9.mjs +1 -315
  44. package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
  45. package/dist/packem_chunks/runDevCommand.mjs +8 -0
  46. package/dist/packem_chunks/runInitCommand.mjs +423 -1447
  47. package/dist/packem_chunks/runResetCommand.mjs +1 -41
  48. package/dist/packem_chunks/runRpcCommand.mjs +1 -68
  49. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck9tABfr.mjs +1 -0
  50. package/dist/packem_shared/EXIT_CODE-DQVhSE-1.mjs +1 -0
  51. package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
  52. package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
  53. package/dist/packem_shared/api-spec-lYyq4vL6.mjs +1 -0
  54. package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
  55. package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
  56. package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
  57. package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
  58. package/dist/packem_shared/cli-manifest-DoQ_3bzs.mjs +1 -0
  59. package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
  60. package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
  61. package/dist/packem_shared/command-BpMv2n0o.mjs +1 -0
  62. package/dist/packem_shared/commands-CkuCMUP_.mjs +13 -0
  63. package/dist/packem_shared/createLogger-4TujevyB.mjs +2 -0
  64. package/dist/packem_shared/createRecordingSpawner-DchLi0gR.mjs +1 -0
  65. package/dist/packem_shared/deploy-target-resolve-BTCW1Iv4.mjs +1 -0
  66. package/dist/packem_shared/diffSnapshots-Y1G8am6C.mjs +7 -0
  67. package/dist/packem_shared/docker-DpVxvYpL.mjs +1 -0
  68. package/dist/packem_shared/export-rows-EzdNDYpa.mjs +3 -0
  69. package/dist/packem_shared/free-port-DjbFhaaT.mjs +1 -0
  70. package/dist/packem_shared/health-probe-BLnoq_oC.mjs +13 -0
  71. package/dist/packem_shared/import-3Y59Ry9F.mjs +13 -0
  72. package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
  73. package/dist/packem_shared/insertSchemaExtension-CbTH3lCQ.mjs +10 -0
  74. package/dist/packem_shared/js-literal-FWLi94Lh.mjs +1 -0
  75. package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
  76. package/dist/packem_shared/mcp-config-file-GR1Y_CVw.mjs +2 -0
  77. package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
  78. package/dist/packem_shared/output-format-DWe_tkTR.mjs +2 -0
  79. package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
  80. package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
  81. package/dist/packem_shared/plan-pog5AvMk.mjs +5 -0
  82. package/dist/packem_shared/planDevCommand-Bl6OrwXm.mjs +1 -0
  83. package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
  84. package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
  85. package/dist/packem_shared/render-lunora-error-JsLelWwR.mjs +2 -0
  86. package/dist/packem_shared/resolve-Bdc9KEUv.mjs +5 -0
  87. package/dist/packem_shared/resolve-target-BRvw8Pf7.mjs +1 -0
  88. package/dist/packem_shared/runAddCommand-gThKW-ZB.mjs +1 -0
  89. package/dist/packem_shared/runDeployCommand-ByBuze9W.mjs +1 -0
  90. package/dist/packem_shared/runExportCommand-_PtkJJDT.mjs +5 -0
  91. package/dist/packem_shared/runMigrateGenerateCommand-CguLEIZp.mjs +11 -0
  92. package/dist/packem_shared/schemaIrToSnapshot-Ck7DSPdf.mjs +1 -0
  93. package/dist/packem_shared/shared-CV7ESvuj.mjs +2 -0
  94. package/dist/packem_shared/storage-3GOIFVZm.mjs +1 -0
  95. package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
  96. package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
  97. package/dist/packem_shared/vectorize-metadata-DK61NmVx.mjs +2 -0
  98. package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
  99. package/dist/packem_shared/wrangler-secrets-C9siMBwN.mjs +1 -0
  100. package/package.json +45 -19
  101. package/skills/README.md +36 -17
  102. package/skills/lunora/SKILL.md +158 -65
  103. package/skills/lunora-cloudflare-audit/SKILL.md +164 -0
  104. package/skills/lunora-create-package/SKILL.md +98 -84
  105. package/skills/lunora-deploy/SKILL.md +212 -102
  106. package/skills/lunora-functions/SKILL.md +367 -60
  107. package/skills/lunora-migration-helper/SKILL.md +194 -149
  108. package/skills/lunora-performance-audit/SKILL.md +137 -95
  109. package/skills/lunora-quickstart/SKILL.md +136 -136
  110. package/skills/lunora-realtime/SKILL.md +219 -97
  111. package/skills/lunora-setup-auth/SKILL.md +203 -101
  112. package/skills/lunora-setup-hyperdrive/SKILL.md +153 -110
  113. package/skills/lunora-setup-hyperdrive-global/SKILL.md +152 -94
  114. package/skills/lunora-setup-mail/SKILL.md +87 -113
  115. package/skills/lunora-setup-scheduler/SKILL.md +133 -97
  116. package/skills/lunora-setup-storage/SKILL.md +189 -95
  117. package/dist/packem_chunks/planDevCommand.mjs +0 -541
  118. package/dist/packem_chunks/runDeployCommand.mjs +0 -594
  119. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
  120. package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
  121. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
  122. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
  123. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
  124. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
  125. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
  126. package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
  127. package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
  128. package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
  129. package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
  130. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
  131. package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
  132. package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
  133. package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
  134. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
  135. package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
  136. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  137. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
  138. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
  139. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
  140. package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
  141. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
  142. package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
  143. package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
  144. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
  145. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
  146. package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +0 -47
@@ -1,90 +1,65 @@
1
1
  ---
2
2
  name: lunora-setup-hyperdrive
3
- description: Connects an existing Postgres/MySQL database to a Lunora app from an action via Cloudflare Hyperdrive. Use for `@lunora/hyperdrive`, `ctx.sql`, `createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`, the `HYPERDRIVE` binding, `wrangler hyperdrive create`, and the action-only / non-reactive guardrails (live queries don't track external writes).
3
+ description: Connects an existing, externally owned Postgres/MySQL database to a Lunora app through Cloudflare Hyperdrive. Lunora does not own its schema. Covers `@lunora/hyperdrive`, the action-only, non-reactive `ctx.sql` client (`createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`, wired with the app builder's `.hyperdrive()`), the `HYPERDRIVE` binding and `wrangler hyperdrive create`, and making external rows live through a projection or a `.source()` ingest table. Use when the user wants to "query our existing Postgres", "read the legacy DB from an action", "connect Neon/RDS/Supabase", or sync external rows into Lunora, or sees `ctx.sql` / `hyperdrive_outside_action` errors. For storing Lunora's own `.global()` tables in Postgres/MySQL with reactive live queries, use `lunora-setup-hyperdrive-global` instead.
4
4
  ---
5
5
 
6
6
  # Lunora Setup Hyperdrive
7
7
 
8
- Wire an **existing** Postgres/MySQL database into a Lunora app using
9
- `@lunora/hyperdrive`, which surfaces a [Cloudflare
10
- Hyperdrive](https://developers.cloudflare.com/hyperdrive/) binding's connection
11
- string and a driver-agnostic `ctx.sql` client — usable **only inside an
12
- `action`**.
13
-
14
- > **Integrate, don't replace.** Hyperdrive talks to a database Lunora has no
15
- > visibility into. Queries through `ctx.sql` are **non-deterministic** (forbidden
16
- > in `query`/`mutation`, enforced by the `hyperdrive_outside_action` advisor
17
- > lint) and external writes are **invisible to Lunora live queries** —
18
- > subscriptions will NOT re-run when external rows change. Use it to read/write a
19
- > legacy DB from an action; if you want that data reactive, write a projection
20
- > into a `defineSchema` DO/D1 table.
21
-
22
- ## When to Use
23
-
24
- - Reading/writing an existing Postgres or MySQL database from a Lunora action.
25
- - Incrementally migrating off a legacy DB while standing up Lunora alongside it.
26
- - Backfilling/syncing external rows into reactive Lunora tables (projection).
27
-
28
- ## When Not to Use
29
-
30
- - You want the external data to be **reactive** out of the box — it can't be;
31
- Hyperdrive writes don't hit the change-feed. Project into a `defineSchema`
32
- table instead.
33
- - You're reaching for `ctx.sql` inside a `query` or `mutation` — forbidden by
34
- design (the advisor lint flags it). Move the access into an `action`.
35
- - The app has no Lunora backend yet — use `lunora-quickstart` first.
36
- - You'd be replacing `defineSchema` entirely — then you lose realtime, OCC,
37
- optimistic updates, the offline queue, and the advisors. Use Lunora's own data
38
- layer for app state.
39
-
40
- ## Workflow
41
-
42
- 1. Install `@lunora/hyperdrive` and one SQL driver.
43
- 2. Create the Hyperdrive config (`wrangler hyperdrive create`) and add the
44
- `HYPERDRIVE` binding to `wrangler.jsonc`.
45
- 3. Regenerate types with `lunora codegen` so `ctx.sql` appears on `ActionCtx`.
46
- 4. Read/write the external DB from an action via `ctx.sql`.
47
- 5. (Optional) Project external rows into a `defineSchema` table to make them
48
- reactive.
49
-
50
- ## Step 1: Install the package + a driver
8
+ `@lunora/hyperdrive` exposes a Cloudflare Hyperdrive binding as a
9
+ driver-agnostic `ctx.sql` client that is available only inside an `action`. It integrates
10
+ a database Lunora doesn't own. Don't use it to replace `defineSchema`, because you'd lose
11
+ realtime, OCC, optimistic updates, the offline queue and the advisors.
12
+
13
+ > **`ctx.sql` is not reactive.** Lunora cannot see this database, so subscriptions
14
+ > do not re-run when its rows change, including rows you write through `ctx.sql`.
15
+ > Queries are non-deterministic, so `ctx.sql` exists only on `ActionCtx`.
16
+ > The `hyperdrive_outside_action` advisor lint flags any use in a
17
+ > `query`/`mutation`. To make the data live, see Step 5. If you instead want Lunora-owned tables
18
+ > stored in Postgres/MySQL and fully reactive, use `lunora-setup-hyperdrive-global`.
19
+
20
+ ## Step 1: Install the package + one driver
51
21
 
52
22
  ```bash
53
23
  pnpm add @lunora/hyperdrive
54
- # choose ONE driver (optional peer deps — none is bundled):
55
- pnpm add postgres # postgres.js → fromPostgresJs ($1, $2 placeholders)
56
- # or
57
- pnpm add pg # node-postgres → fromNodePg ($1, $2 placeholders)
58
- # or
59
- pnpm add mysql2 # mysql2 → fromMysql2 (? placeholders)
24
+ pnpm add postgres # → fromPostgresJs ($1, $2 placeholders)
25
+ # or: pnpm add pg → fromNodePg ($1, $2)
26
+ # or: pnpm add mysql2 → fromMysql2 (?)
60
27
  ```
61
28
 
29
+ The drivers are optional peer dependencies, so none ships with the package.
30
+ The package never rewrites SQL, so use your driver's native placeholders.
31
+
62
32
  ## Step 2: Create the binding
63
33
 
34
+ Nothing to write by hand: `lunora dev` and `lunora deploy` add a `HYPERDRIVE` binding
35
+ to `wrangler.jsonc` with a placeholder `id`.
36
+
37
+ - **Dev.** `ctx.sql` runs on an embedded Postgres (PGlite), data in
38
+ `.wrangler/state/lunora-pglite`. Install its two optional packages:
39
+
64
40
  ```bash
65
- wrangler hyperdrive create my-db --connection-string="postgres://user:pass@host:5432/db" # gitleaks:allow -- placeholder, not a real secret
41
+ pnpm add -D @electric-sql/pglite @electric-sql/pglite-socket
66
42
  ```
67
43
 
68
- This prints an `id`. Add the binding to `wrangler.jsonc`, using
69
- `localConnectionString` so `lunora dev` connects to your DB directly (no edge
70
- proxy locally):
71
-
72
- ```jsonc
73
- {
74
- "hyperdrive": [
75
- {
76
- "binding": "HYPERDRIVE",
77
- "id": "<the id from the command above>",
78
- "localConnectionString": "postgres://user:pass@localhost:5432/db", // gitleaks:allow -- placeholder, not a real secret
79
- },
80
- ],
81
- }
44
+ To use your own database, set `localConnectionString` on the binding or
45
+ `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING>`; neither is overridden.
46
+
47
+ - **Deploy.** Give `lunora deploy` the origin and a token that may edit Hyperdrive:
48
+
49
+ ```bash
50
+ LUNORA_HYPERDRIVE_URL="postgres://user:pass@host:5432/db" lunora deploy # gitleaks:allow -- placeholder, not a real secret
82
51
  ```
83
52
 
84
- Lunora validates this binding (errors if `binding` is missing; warns if `id` is
85
- empty/placeholder — it can't connect), but it does **not** auto-write the `id`:
86
- that's a remote resource only `wrangler hyperdrive create` can mint. Importing
87
- `@lunora/hyperdrive` surfaces a hint reminding you to add it.
53
+ (`CLOUDFLARE_API_TOKEN` with Hyperdrive: Edit, and `CLOUDFLARE_ACCOUNT_ID`; the variable is
54
+ `LUNORA_<BINDING>_URL`.) Lunora creates the config, or reuses one of the same name, and writes
55
+ its `id` into `wrangler.jsonc`. The connection string is never written or logged.
56
+
57
+ To manage the config yourself, run `wrangler hyperdrive create my-db --connection-string=...`
58
+ and put the `id` in the binding. Validation fails without a `binding`.
59
+
60
+ Hyperdrive caches read results (up to 60 s plus 15 s stale by default) and a write does not
61
+ invalidate them. For reads that must see your own writes, add a second config created with
62
+ `--caching-disabled` and bind it under another name.
88
63
 
89
64
  ## Step 3: Regenerate types
90
65
 
@@ -92,63 +67,131 @@ that's a remote resource only `wrangler hyperdrive create` can mint. Importing
92
67
  lunora codegen
93
68
  ```
94
69
 
95
- When codegen sees `ctx.sql` used, it adds `sql: SqlClient` to **`ActionCtx`
96
- only** — never `QueryCtx`/`MutationCtx` — with a JSDoc restating the
97
- determinism/realtime caveat.
70
+ When `ctx.sql` is used, codegen adds `readonly sql: SqlClient` to `ActionCtx` only. It also
71
+ emits a `.hyperdrive()` method on the generated app builder.
98
72
 
99
- ## Step 4: Use `ctx.sql` from an action
73
+ ## Step 4: Wire the client on the app builder
100
74
 
101
- ```ts
75
+ `ctx.sql` is `readonly`, so assigning it in a handler is a `TS2540` error.
76
+ Codegen can't build the client because it depends on your driver, so supply a
77
+ thunk instead. It runs once per shard construction, not per request:
78
+
79
+ ```ts title="src/server.ts"
80
+ import type { HyperdriveLike } from "@lunora/hyperdrive";
102
81
  import { createHyperdrive, fromPostgresJs } from "@lunora/hyperdrive";
103
- import postgres from "postgres";
104
82
 
105
- import { action, v } from "@lunora/server";
83
+ import { defineApp } from "../lunora/_generated/app.js";
106
84
 
107
- export const listLegacyOrders = action.input({ orgId: v.string() }).action(async ({ ctx, args: { orgId } }) => {
108
- const { connectionString } = createHyperdrive(ctx.env.HYPERDRIVE);
109
- ctx.sql = fromPostgresJs(postgres(connectionString));
85
+ const app = defineApp<Env>()
86
+ .shard((env) => env.SHARD)
87
+ .hyperdrive((env) =>
88
+ fromPostgresJs(async () => {
89
+ const { default: postgres } = await import("postgres");
90
+
91
+ return postgres(createHyperdrive(env.HYPERDRIVE as HyperdriveLike).connectionString, { fetch_types: false, max: 5 });
92
+ }),
93
+ )
94
+ .build();
95
+
96
+ export const ShardDO = app.ShardDO;
97
+ ```
98
+
99
+ The adapters take a loader, not a driver. It runs on the first query, so the
100
+ driver stays out of the Worker's startup graph; never `import postgres from
101
+ "postgres"` statically in the entry. For `pg`, the loader may `await
102
+ client.connect()` before returning.
103
+
104
+ Then query from any action:
110
105
 
111
- return ctx.sql.query<{ id: string; total: number }>("select id, total from orders where org = $1", [orgId]);
106
+ ```ts
107
+ import { LunoraError } from "lunorash/server";
108
+
109
+ import { action, v } from "#lunora/_generated/server.js";
110
+
111
+ export const listLegacyOrders = action.input({ orgId: v.string() }).action(async ({ ctx, args: { orgId } }) => {
112
+ if (!ctx.auth.userId) {
113
+ throw new LunoraError("UNAUTHORIZED", "not signed in");
114
+ }
115
+
116
+ // `orgId` comes from the client: the join scopes it to orgs the caller belongs to.
117
+ return ctx.sql.query<{ id: string; total: number }>(
118
+ "select o.id, o.total from orders o join org_members m on m.org_id = o.org where o.org = $1 and m.user_id = $2",
119
+ [orgId, ctx.auth.userId],
120
+ );
112
121
  });
113
122
  ```
114
123
 
115
- The package never rewrites SQL — use your driver's native placeholders (`$1` for
116
- Postgres, `?` for MySQL).
124
+ **Verify:** call the action from the studio's function runner and check that it returns rows.
125
+
126
+ ## Step 5 (optional): Make external data reactive
117
127
 
118
- ## Step 5 (optional): Make it reactive via a projection
128
+ Pick one approach:
119
129
 
120
- External writes don't hit the change-feed, so a subscription won't re-fire on a
121
- Postgres change. To make external data reactive, write a projection into a
122
- `defineSchema` table from the same action — that write _is_ tracked:
130
+ **A. Projection from an action.** This works when your app is the writer. Write the row into a
131
+ `defineSchema` table through a mutation. That write is tracked:
123
132
 
124
133
  ```ts
134
+ import { LunoraError } from "lunorash/server";
135
+
136
+ import { internal } from "#lunora/_generated/internal.js";
137
+
125
138
  const [row] = await ctx.sql.query<{ id: string; total: number }>("select id, total from orders where id = $1", [id]);
126
139
 
127
- // This write re-runs live queries reading `orders`:
128
- await ctx.runMutation("orders:upsert", { id: row.id, total: row.total });
140
+ if (!row) {
141
+ throw new LunoraError("NOT_FOUND", `order ${id} not found`);
142
+ }
143
+
144
+ // Re-runs live queries over `orders`. ctx.run* takes a generated reference, not a "file:fn" string.
145
+ await ctx.runMutation(internal.orders.upsert, { id: row.id, total: row.total });
129
146
  ```
130
147
 
148
+ **B. A `.source()` table.** This works when other systems also write the database. Lunora polls
149
+ the external query on the shard's alarm, diffs the result, materializes it into the
150
+ DO's SQLite and notifies subscribers, so you don't write an action or a cron:
151
+
152
+ ```ts
153
+ // lunora/schema.ts
154
+ messages: defineTable({ body: v.string(), channelId: v.string() })
155
+ .shardBy("channelId")
156
+ .source({
157
+ binding: "HYPERDRIVE",
158
+ query: 'select id, body, channel_id as "channelId" from messages where channel_id = $1',
159
+ tenantBy: (shardKey) => [shardKey], // required under .shardBy(): the tenant boundary
160
+ }),
161
+ ```
162
+
163
+ ```ts
164
+ // worker entry: one client per binding name
165
+ .sourceClient((env, binding) => fromPostgresJs(async () => {
166
+ const { default: postgres } = await import("postgres");
167
+
168
+ return postgres((env[binding] as { connectionString: string }).connectionString, { fetch_types: false, max: 5 });
169
+ }))
170
+ ```
171
+
172
+ How `.source()` behaves:
173
+
174
+ - Freshness depends on the poll interval: every alarm tick by default,
175
+ `refresh: { everyMs }` to throttle it, or `"manual"`.
176
+ - The table is read-only from Lunora: rows come only from the ingest loop.
177
+ - The default `mode: "full-pull"` re-reads the whole slice on every tick, which
178
+ works up to about 10k rows. For larger slices, use `mode: "incremental"` with a
179
+ `cursor`, plus either `reconcileEveryMs` or `softDeleteColumn` so deletes are
180
+ still detected.
181
+ - A source table can't also be `.global()`.
182
+
131
183
  ## Common Pitfalls
132
184
 
133
- 1. **`ctx.sql` in a query/mutation.** It's action-only by design; the
134
- `hyperdrive_outside_action` advisor lint flags it. Move the SQL into an
135
- action.
136
- 2. **Expecting external writes to be reactive.** They aren't — Lunora can't see
137
- them. Project into a `defineSchema` table if you need reactivity.
138
- 3. **Placeholder / empty `id`.** A Hyperdrive binding with no real `id` can't
139
- connect (Lunora warns). Run `wrangler hyperdrive create` and paste the id.
140
- 4. **Bundling a driver as a hard dependency.** Drivers are optional peer deps —
141
- install the one you use; don't assume one ships with the package.
142
- 5. **Wrong placeholder syntax.** `$1, $2` for Postgres drivers, `?` for mysql2.
143
- The package does not rewrite SQL.
144
-
145
- ## Checklist
146
-
147
- - [ ] `@lunora/hyperdrive` + one driver (`postgres`/`pg`/`mysql2`) installed.
148
- - [ ] `wrangler hyperdrive create` run; `HYPERDRIVE` binding added to
149
- `wrangler.jsonc` with a real `id` and a `localConnectionString` for dev.
150
- - [ ] `lunora codegen` run so `ctx.sql` is typed on `ActionCtx`.
151
- - [ ] All `ctx.sql` access lives in actions (advisor clean — no
152
- `hyperdrive_outside_action`).
153
- - [ ] If reactivity is needed, external rows are projected into a `defineSchema`
154
- table.
185
+ 1. **`ctx.sql` in a query/mutation.** It's action-only. Move the SQL into an action.
186
+ 2. **Expecting `ctx.sql` writes to update subscriptions.** They don't. Use Step 5.
187
+ 3. **Placeholder `id` at deploy.** The deploy stops until `LUNORA_<BINDING>_URL` is set
188
+ (Lunora then creates the config) or the real `id` is pasted in. An empty `id` is not
189
+ provisioned: wrangler rejects it, so remove the entry and let Lunora add it again.
190
+ 4. **Wrong placeholders.** Use `$1` for `postgres`/`pg` and `?` for `mysql2`.
191
+ 5. **Array or JSON params on postgres.js with `fetch_types: false`.** A JS array bound to
192
+ `= ANY($1)` fails as a malformed array literal: bind `JSON.stringify(list)` to
193
+ `IN (SELECT jsonb_array_elements_text($1::text::jsonb))`. Cast JSON params `$1::text::jsonb`,
194
+ since a bare `$1::jsonb` gets your string JSON-encoded twice.
195
+ 6. **Unscoped `.source()` under `.shardBy()`.** Without `tenantBy`, every tenant's
196
+ rows are copied into each shard. `defineSchema` throws, and the
197
+ `external_source_unscoped` lint fails the build.