@raven.js/cli 1.1.2 → 1.2.0

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.
@@ -0,0 +1,271 @@
1
+ # OVERVIEW
2
+
3
+ @raven.js/sql is a RavenJS plugin that wraps [Bun's native SQL bindings](https://bun.com/docs/runtime/sql), exposing a single `Bun.SQL` client via the framework's dependency injection (AppState).
4
+
5
+ **Features**:
6
+ - **Unified API**: One client for PostgreSQL, MySQL, or SQLite using the same tagged template literal interface.
7
+ - **DI Integration**: The SQL client is stored in AppState; handlers obtain it via the state returned from `register()`.
8
+ - **Multiple clients**: Register the plugin multiple times for different databases; each registration gets its own state and connection pool.
9
+ - **Zero Boilerplate**: No manual client creation in handlers — use the injected client with `` sql`SELECT ...` ``.
10
+
11
+ ---
12
+
13
+ # ARCHITECTURE
14
+
15
+ **Plugin lifecycle**:
16
+
17
+ ```
18
+ app.register(sqlPlugin(config))
19
+
20
+
21
+ [load()] ← Creates new Bun.SQL(config), sets on ClientState
22
+
23
+
24
+ register() resolves ← Returns [ClientState] to caller
25
+
26
+
27
+ [handler] ← sql = MyDB.getOrFailed(); sql`SELECT ...`
28
+ ```
29
+
30
+ ---
31
+
32
+ # CORE CONCEPTS
33
+
34
+ ## `sqlPlugin(config)`
35
+
36
+ The plugin factory accepts `Bun.SQL.Options`: either a connection string or an options object (e.g. `adapter`, `hostname`, `port`, `database`, `username`, `password`). The same options are passed to `new Bun.SQL(config)`.
37
+
38
+ ```typescript
39
+ // Connection string (PostgreSQL, MySQL, or SQLite)
40
+ sqlPlugin("postgres://user:pass@localhost:5432/mydb");
41
+ sqlPlugin("mysql://user:pass@localhost:3306/mydb");
42
+ sqlPlugin("sqlite://./data.db");
43
+
44
+ // Options object (e.g. MySQL)
45
+ sqlPlugin({
46
+ adapter: "mysql",
47
+ hostname: "localhost",
48
+ port: 3306,
49
+ database: "myapp",
50
+ username: "dbuser",
51
+ password: "secret",
52
+ });
53
+ ```
54
+
55
+ ## Obtaining the client in handlers
56
+
57
+ The plugin declares one AppState (`Bun.SQL`). You **must** destructure that state from the return value of `app.register()` and use it in routes. The client is shared across the app (single instance).
58
+
59
+ A common convention is to name the state after the database (e.g. database `mydb` → state `MyDB`), then get the client with `getOrFailed()`:
60
+
61
+ ```typescript
62
+ const [MyDB] = await app.register(sqlPlugin("postgres://localhost/mydb"));
63
+
64
+ app.get("/users", async () => {
65
+ const sql = MyDB.getOrFailed();
66
+ const users = await sql`SELECT * FROM users LIMIT 10`;
67
+ return Response.json(users);
68
+ });
69
+ ```
70
+
71
+ ## Multiple clients
72
+
73
+ You can register the plugin **multiple times** with different configs. Each call to `sqlPlugin(config)` creates a new AppState and a new `Bun.SQL` instance, so you get one state per database. Destructure a different name from each `register()` and use the corresponding state in handlers.
74
+
75
+ ```typescript
76
+ const [MyDB] = await app.register(sqlPlugin("postgres://localhost/mydb"));
77
+ const [AnalyticsDB] = await app.register(sqlPlugin("mysql://localhost/analytics"));
78
+ const [CacheDB] = await app.register(sqlPlugin("sqlite://./cache.db"));
79
+
80
+ app.get("/users", async () => {
81
+ const sql = MyDB.getOrFailed();
82
+ return Response.json(await sql`SELECT * FROM users`);
83
+ });
84
+
85
+ app.get("/stats", async () => {
86
+ const sql = AnalyticsDB.getOrFailed();
87
+ return Response.json(await sql`SELECT * FROM events LIMIT 100`);
88
+ });
89
+ ```
90
+
91
+ Each registration is independent: different adapters (PostgreSQL, MySQL, SQLite), different connection strings, and separate connection pools.
92
+
93
+ ## Using the client (Bun.SQL)
94
+
95
+ The value from `state.getOrFailed()` is a `Bun.SQL` instance. Use tagged template literals for queries (parameters are safely bound; no manual escaping). See [Bun SQL docs](https://bun.com/docs/runtime/sql) for transactions, bulk inserts, and adapter-specific options.
96
+
97
+ ```typescript
98
+ const sql = MyDB.getOrFailed();
99
+
100
+ // Parameterized query
101
+ const row = await sql`SELECT * FROM users WHERE id = ${userId}`;
102
+
103
+ // Transaction
104
+ await sql.begin(async (tx) => {
105
+ await tx`INSERT INTO logs (msg) VALUES (${msg})`;
106
+ await tx`UPDATE counters SET n = n + 1 WHERE id = ${id}`;
107
+ });
108
+ ```
109
+
110
+ ---
111
+
112
+ # DESIGN DECISIONS
113
+
114
+ ## Why a plugin instead of creating `Bun.SQL` in handlers?
115
+
116
+ - **Single instance**: One connection pool per app; creating `new Bun.SQL()` in every handler would open many connections.
117
+ - **DI consistency**: RavenJS uses AppState for app-scoped dependencies; the SQL client fits this model and is available anywhere in the request chain without passing arguments.
118
+ - **Testability**: In tests, you can register a different plugin (or mock state) without changing handler code.
119
+
120
+ ## Why return the state from `register()`?
121
+
122
+ The state is created inside the plugin factory, so the only way for the app to get a reference is via the tuple returned by `register()`. This follows the same pattern as other RavenJS plugins (see [PLUGIN.md](https://github.com/bytedance/ravenjs/blob/main/modules/core/PLUGIN.md) in core).
123
+
124
+ ---
125
+
126
+ # GOTCHAS
127
+
128
+ ## 1. Always `await app.register(sqlPlugin(...))` and destructure the state
129
+
130
+ If you don't capture the state, you cannot access the client in handlers.
131
+
132
+ ```typescript
133
+ // ❌ Wrong: state is lost
134
+ await app.register(sqlPlugin(process.env.DATABASE_URL!));
135
+
136
+ app.get("/users", () => {
137
+ // No way to get the client
138
+ });
139
+
140
+ // ✓ Correct: destructure state (e.g. name after db) and use in handlers
141
+ const [MyDB] = await app.register(sqlPlugin(process.env.DATABASE_URL!));
142
+ app.get("/users", async () => {
143
+ const sql = MyDB.getOrFailed();
144
+ const users = await sql`SELECT * FROM users`;
145
+ return Response.json(users);
146
+ });
147
+ ```
148
+
149
+ ## 2. Register the plugin before defining routes that use the client
150
+
151
+ Plugins are loaded when you call `register()`. As with other plugins, ensure registration happens before routes that depend on the SQL state.
152
+
153
+ ## 3. Bun.SQL requires Bun runtime
154
+
155
+ This plugin depends on `Bun.SQL`, which is only available in the Bun runtime. Do not use it in Node or other runtimes.
156
+
157
+ ---
158
+
159
+ # USAGE EXAMPLES
160
+
161
+ ## Minimal (PostgreSQL with env URL)
162
+
163
+ ```typescript
164
+ import { Raven } from "@raven.js/core";
165
+ import { sqlPlugin } from "@raven.js/sql";
166
+
167
+ const app = new Raven();
168
+ const [MyDB] = await app.register(sqlPlugin(process.env.DATABASE_URL!));
169
+
170
+ app.get("/", async () => {
171
+ const sql = MyDB.getOrFailed();
172
+ const rows = await sql`SELECT 1 as num`;
173
+ return Response.json(rows);
174
+ });
175
+
176
+ Bun.serve({ fetch: (req) => app.handle(req), port: 3000 });
177
+ ```
178
+
179
+ ## Query with parameters
180
+
181
+ ```typescript
182
+ import { ParamsState } from "@raven.js/core";
183
+
184
+ const [AppDB] = await app.register(sqlPlugin("sqlite://./app.db"));
185
+
186
+ app.get("/user/:id", async () => {
187
+ const { id } = ParamsState.getOrFailed();
188
+ const sql = AppDB.getOrFailed();
189
+ const [user] = await sql`SELECT * FROM users WHERE id = ${id}`;
190
+ if (!user) return new Response("Not found", { status: 404 });
191
+ return Response.json(user);
192
+ });
193
+ ```
194
+
195
+ ## Transaction
196
+
197
+ ```typescript
198
+ import { BodyState } from "@raven.js/core";
199
+
200
+ app.post("/transfer", async () => {
201
+ const sql = MyDB.getOrFailed();
202
+ const body = BodyState.getOrFailed() as { from: string; to: string; amount: number };
203
+ await sql.begin(async (tx) => {
204
+ await tx`UPDATE accounts SET balance = balance - ${body.amount} WHERE id = ${body.from}`;
205
+ await tx`UPDATE accounts SET balance = balance + ${body.amount} WHERE id = ${body.to}`;
206
+ });
207
+ return new Response(null, { status: 204 });
208
+ });
209
+ ```
210
+
211
+ ## Multiple databases
212
+
213
+ Register the plugin once per database and name each state after that database (e.g. `MyDB`, `AnalyticsDB`).
214
+
215
+ ```typescript
216
+ import { Raven } from "@raven.js/core";
217
+ import { sqlPlugin } from "@raven.js/sql";
218
+
219
+ const app = new Raven();
220
+
221
+ const [MyDB] = await app.register(sqlPlugin(process.env.DATABASE_URL!));
222
+ const [AnalyticsDB] = await app.register(sqlPlugin(process.env.ANALYTICS_DATABASE_URL!));
223
+
224
+ app.get("/users", async () => {
225
+ const sql = MyDB.getOrFailed();
226
+ const users = await sql`SELECT id, name FROM users`;
227
+ return Response.json(users);
228
+ });
229
+
230
+ app.get("/metrics", async () => {
231
+ const sql = AnalyticsDB.getOrFailed();
232
+ const rows = await sql`SELECT * FROM metrics WHERE ts > NOW() - INTERVAL 1 DAY`;
233
+ return Response.json(rows);
234
+ });
235
+ ```
236
+
237
+ ---
238
+
239
+ # ANTI-PATTERNS
240
+
241
+ ## Do not create a new `Bun.SQL` inside handlers
242
+
243
+ ```typescript
244
+ // ❌ New client per request — no pooling, wasteful
245
+ app.get("/users", async () => {
246
+ const db = new Bun.SQL(process.env.DATABASE_URL!);
247
+ const users = await db`SELECT * FROM users`;
248
+ return Response.json(users);
249
+ });
250
+
251
+ // ✓ Use the injected client: name state after db, then get client
252
+ const [MyDB] = await app.register(sqlPlugin(process.env.DATABASE_URL!));
253
+ app.get("/users", async () => {
254
+ const sql = MyDB.getOrFailed();
255
+ const users = await sql`SELECT * FROM users`;
256
+ return Response.json(users);
257
+ });
258
+ ```
259
+
260
+ ## Do not forget to await `register()`
261
+
262
+ ```typescript
263
+ // ❌ Plugin may not have run yet; client not set
264
+ app.register(sqlPlugin(config));
265
+ app.get("/", async () => {
266
+ const sql = MyDB.getOrFailed(); // may throw or use undefined state
267
+ });
268
+
269
+ // ✓ Always await and destructure the state
270
+ const [MyDB] = await app.register(sqlPlugin(config));
271
+ ```
@@ -0,0 +1,14 @@
1
+ import { createAppState, definePlugin } from "@raven.js/core";
2
+
3
+ export function sqlPlugin(config: Bun.SQL.Options) {
4
+ const ClientState = createAppState<Bun.SQL>();
5
+
6
+ return definePlugin({
7
+ name: "raven-sql",
8
+ states: [ClientState],
9
+ load() {
10
+ const client = new Bun.SQL(config);
11
+ ClientState.set(client);
12
+ },
13
+ });
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raven.js/cli",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "CLI tool for RavenJS framework",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@clack/prompts": "^1.0.1",
18
- "cac": "^6.7.14",
18
+ "commander": "^12.0.0",
19
19
  "picocolors": "^1.1.1",
20
20
  "semver": "^7.7.4",
21
21
  "yaml": "^2.6.0"