@lunora/cli 1.0.0-alpha.47 → 1.0.0-alpha.470
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.
- package/LICENSE.md +33 -0
- package/README.md +3 -1
- package/dist/bin.mjs +3 -10
- package/dist/cli.d.mts +31 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.mjs +1 -0
- package/dist/index.d.mts +1167 -416
- package/dist/index.d.ts +1167 -416
- package/dist/index.mjs +1 -19
- package/dist/packem_chunks/cli.mjs +2 -0
- package/dist/packem_chunks/dispatch.mjs +1 -0
- package/dist/packem_chunks/handler.mjs +55 -158
- package/dist/packem_chunks/handler10.mjs +2 -16
- package/dist/packem_chunks/handler11.mjs +1 -22
- package/dist/packem_chunks/handler12.mjs +1 -192
- package/dist/packem_chunks/handler13.mjs +1 -131
- package/dist/packem_chunks/handler14.mjs +3 -65
- package/dist/packem_chunks/handler15.mjs +1 -58
- package/dist/packem_chunks/handler16.mjs +1 -80
- package/dist/packem_chunks/handler17.mjs +1 -43
- package/dist/packem_chunks/handler18.mjs +1 -105
- package/dist/packem_chunks/handler19.mjs +1 -170
- package/dist/packem_chunks/handler2.mjs +1 -114
- package/dist/packem_chunks/handler20.mjs +1 -94
- package/dist/packem_chunks/handler21.mjs +7 -94
- package/dist/packem_chunks/handler22.mjs +1 -0
- package/dist/packem_chunks/handler23.mjs +3 -0
- package/dist/packem_chunks/handler24.mjs +3 -0
- package/dist/packem_chunks/handler25.mjs +2 -0
- package/dist/packem_chunks/handler26.mjs +1 -0
- package/dist/packem_chunks/handler27.mjs +2 -0
- package/dist/packem_chunks/handler28.mjs +99 -0
- package/dist/packem_chunks/handler29.mjs +3 -0
- package/dist/packem_chunks/handler3.mjs +1 -204
- package/dist/packem_chunks/handler30.mjs +15 -0
- package/dist/packem_chunks/handler31.mjs +1 -0
- package/dist/packem_chunks/handler32.mjs +1 -0
- package/dist/packem_chunks/handler4.mjs +1 -33
- package/dist/packem_chunks/handler5.mjs +1 -49
- package/dist/packem_chunks/handler6.mjs +3 -91
- package/dist/packem_chunks/handler7.mjs +1 -42
- package/dist/packem_chunks/handler8.mjs +1 -174
- package/dist/packem_chunks/handler9.mjs +1 -315
- package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
- package/dist/packem_chunks/runDevCommand.mjs +6 -0
- package/dist/packem_chunks/runInitCommand.mjs +423 -1447
- package/dist/packem_chunks/runResetCommand.mjs +1 -41
- package/dist/packem_chunks/runRpcCommand.mjs +1 -68
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck9tABfr.mjs +1 -0
- package/dist/packem_shared/EXIT_CODE-DQVhSE-1.mjs +1 -0
- package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
- package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
- package/dist/packem_shared/api-spec-lYyq4vL6.mjs +1 -0
- package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
- package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
- package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
- package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
- package/dist/packem_shared/cli-manifest-DoQ_3bzs.mjs +1 -0
- package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
- package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
- package/dist/packem_shared/command-BpMv2n0o.mjs +1 -0
- package/dist/packem_shared/commands-CkuCMUP_.mjs +13 -0
- package/dist/packem_shared/createLogger-4TujevyB.mjs +2 -0
- package/dist/packem_shared/createRecordingSpawner-DchLi0gR.mjs +1 -0
- package/dist/packem_shared/deploy-target-resolve-BTCW1Iv4.mjs +1 -0
- package/dist/packem_shared/diffSnapshots-Y1G8am6C.mjs +7 -0
- package/dist/packem_shared/docker-DpVxvYpL.mjs +1 -0
- package/dist/packem_shared/export-rows-EzdNDYpa.mjs +3 -0
- package/dist/packem_shared/free-port-DjbFhaaT.mjs +1 -0
- package/dist/packem_shared/health-probe-BLnoq_oC.mjs +13 -0
- package/dist/packem_shared/import-3Y59Ry9F.mjs +13 -0
- package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
- package/dist/packem_shared/insertSchemaExtension-CbTH3lCQ.mjs +10 -0
- package/dist/packem_shared/js-literal-FWLi94Lh.mjs +1 -0
- package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
- package/dist/packem_shared/mcp-config-file-GR1Y_CVw.mjs +2 -0
- package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
- package/dist/packem_shared/output-format-DWe_tkTR.mjs +2 -0
- package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
- package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
- package/dist/packem_shared/plan-pog5AvMk.mjs +5 -0
- package/dist/packem_shared/planDevCommand-Bl6OrwXm.mjs +1 -0
- package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
- package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
- package/dist/packem_shared/render-lunora-error-JsLelWwR.mjs +2 -0
- package/dist/packem_shared/resolve-Bdc9KEUv.mjs +5 -0
- package/dist/packem_shared/resolve-target-BRvw8Pf7.mjs +1 -0
- package/dist/packem_shared/runAddCommand-gThKW-ZB.mjs +1 -0
- package/dist/packem_shared/runDeployCommand-CNgOUx91.mjs +1 -0
- package/dist/packem_shared/runExportCommand-_PtkJJDT.mjs +5 -0
- package/dist/packem_shared/runMigrateGenerateCommand-CguLEIZp.mjs +11 -0
- package/dist/packem_shared/schemaIrToSnapshot-Ck7DSPdf.mjs +1 -0
- package/dist/packem_shared/shared-CV7ESvuj.mjs +2 -0
- package/dist/packem_shared/storage-3GOIFVZm.mjs +1 -0
- package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
- package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
- package/dist/packem_shared/vectorize-metadata-DK61NmVx.mjs +2 -0
- package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
- package/dist/packem_shared/wrangler-secrets-C9siMBwN.mjs +1 -0
- package/package.json +45 -19
- package/skills/README.md +36 -17
- package/skills/lunora/SKILL.md +158 -65
- package/skills/lunora-cloudflare-audit/SKILL.md +164 -0
- package/skills/lunora-create-package/SKILL.md +97 -84
- package/skills/lunora-deploy/SKILL.md +212 -102
- package/skills/lunora-functions/SKILL.md +367 -60
- package/skills/lunora-migration-helper/SKILL.md +194 -149
- package/skills/lunora-performance-audit/SKILL.md +137 -95
- package/skills/lunora-quickstart/SKILL.md +136 -136
- package/skills/lunora-realtime/SKILL.md +217 -97
- package/skills/lunora-setup-auth/SKILL.md +203 -101
- package/skills/lunora-setup-hyperdrive/SKILL.md +153 -110
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +152 -94
- package/skills/lunora-setup-mail/SKILL.md +87 -113
- package/skills/lunora-setup-scheduler/SKILL.md +133 -97
- package/skills/lunora-setup-storage/SKILL.md +189 -95
- package/dist/packem_chunks/planDevCommand.mjs +0 -541
- package/dist/packem_chunks/runDeployCommand.mjs +0 -594
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
- package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
- package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
- package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
- package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
- package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
- package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
- package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
- package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
- package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
- package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
- package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
- package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
- package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
- package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
- package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
- 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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
55
|
-
pnpm add
|
|
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
|
-
|
|
41
|
+
pnpm add -D @electric-sql/pglite @electric-sql/pglite-socket
|
|
66
42
|
```
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
96
|
-
|
|
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:
|
|
73
|
+
## Step 4: Wire the client on the app builder
|
|
100
74
|
|
|
101
|
-
|
|
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 {
|
|
83
|
+
import { defineApp } from "../lunora/_generated/app.js";
|
|
106
84
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
128
|
+
Pick one approach:
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|