@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.
- package/README.md +10 -7
- package/dist/raven +33 -109
- package/dist/raven.map +3 -3
- package/dist/registry.json +1 -9
- package/dist/source/core/GUIDE.md +14 -0
- package/dist/source/core/PLUGIN.md +225 -0
- package/dist/source/core/README.md +427 -0
- package/dist/source/core/index.ts +624 -0
- package/dist/source/core/router.ts +128 -0
- package/dist/source/schema-validator/GUIDE.md +12 -0
- package/dist/source/schema-validator/README.md +229 -0
- package/dist/source/schema-validator/index.ts +139 -0
- package/dist/source/schema-validator/standard-schema.ts +76 -0
- package/dist/source/sql/GUIDE.md +12 -0
- package/dist/source/sql/README.md +271 -0
- package/dist/source/sql/index.ts +14 -0
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
-
"
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
19
|
"picocolors": "^1.1.1",
|
|
20
20
|
"semver": "^7.7.4",
|
|
21
21
|
"yaml": "^2.6.0"
|