@basicbenframework/core 0.1.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/.github/workflows/publish.yml +35 -0
- package/README.md +588 -0
- package/bin/cli.js +8 -0
- package/create-basicben-app/index.js +205 -0
- package/create-basicben-app/package.json +30 -0
- package/create-basicben-app/template/.env.example +24 -0
- package/create-basicben-app/template/README.md +59 -0
- package/create-basicben-app/template/basicben.config.js +33 -0
- package/create-basicben-app/template/index.html +54 -0
- package/create-basicben-app/template/migrations/001_create_users.js +15 -0
- package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
- package/create-basicben-app/template/public/.gitkeep +0 -0
- package/create-basicben-app/template/seeds/01_users.js +29 -0
- package/create-basicben-app/template/seeds/02_posts.js +43 -0
- package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
- package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
- package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
- package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
- package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
- package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
- package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
- package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
- package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
- package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
- package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
- package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
- package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
- package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
- package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
- package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
- package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
- package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
- package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
- package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
- package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
- package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
- package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
- package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
- package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
- package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
- package/create-basicben-app/template/src/controllers/PostController.js +86 -0
- package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
- package/create-basicben-app/template/src/helpers/api.js +24 -0
- package/create-basicben-app/template/src/main.jsx +9 -0
- package/create-basicben-app/template/src/middleware/auth.js +16 -0
- package/create-basicben-app/template/src/models/Post.js +63 -0
- package/create-basicben-app/template/src/models/User.js +42 -0
- package/create-basicben-app/template/src/routes/App.jsx +38 -0
- package/create-basicben-app/template/src/routes/api/auth.js +7 -0
- package/create-basicben-app/template/src/routes/api/posts.js +15 -0
- package/create-basicben-app/template/src/routes/api/profile.js +8 -0
- package/create-basicben-app/template/src/server/index.js +16 -0
- package/create-basicben-app/template/vite.config.js +18 -0
- package/database.sqlite +0 -0
- package/my-test-app/.env.example +24 -0
- package/my-test-app/README.md +59 -0
- package/my-test-app/basicben.config.js +33 -0
- package/my-test-app/database.sqlite-shm +0 -0
- package/my-test-app/database.sqlite-wal +0 -0
- package/my-test-app/index.html +54 -0
- package/my-test-app/migrations/001_create_users.js +15 -0
- package/my-test-app/migrations/002_create_posts.js +18 -0
- package/my-test-app/package-lock.json +2160 -0
- package/my-test-app/package.json +29 -0
- package/my-test-app/public/.gitkeep +0 -0
- package/my-test-app/seeds/01_users.js +29 -0
- package/my-test-app/seeds/02_posts.js +43 -0
- package/my-test-app/src/client/components/Alert.jsx +11 -0
- package/my-test-app/src/client/components/Avatar.jsx +11 -0
- package/my-test-app/src/client/components/BackLink.jsx +10 -0
- package/my-test-app/src/client/components/Button.jsx +19 -0
- package/my-test-app/src/client/components/Card.jsx +10 -0
- package/my-test-app/src/client/components/Empty.jsx +6 -0
- package/my-test-app/src/client/components/Input.jsx +12 -0
- package/my-test-app/src/client/components/Loading.jsx +6 -0
- package/my-test-app/src/client/components/Logo.jsx +40 -0
- package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
- package/my-test-app/src/client/components/NavLink.jsx +10 -0
- package/my-test-app/src/client/components/PageHeader.jsx +8 -0
- package/my-test-app/src/client/components/PostCard.jsx +19 -0
- package/my-test-app/src/client/components/Textarea.jsx +12 -0
- package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
- package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
- package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
- package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
- package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
- package/my-test-app/src/client/pages/Auth.jsx +55 -0
- package/my-test-app/src/client/pages/Authentication.jsx +236 -0
- package/my-test-app/src/client/pages/Database.jsx +426 -0
- package/my-test-app/src/client/pages/Feed.jsx +34 -0
- package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
- package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
- package/my-test-app/src/client/pages/Home.jsx +206 -0
- package/my-test-app/src/client/pages/PostForm.jsx +69 -0
- package/my-test-app/src/client/pages/Posts.jsx +59 -0
- package/my-test-app/src/client/pages/Profile.jsx +68 -0
- package/my-test-app/src/client/pages/Routing.jsx +207 -0
- package/my-test-app/src/client/pages/Testing.jsx +251 -0
- package/my-test-app/src/client/pages/Validation.jsx +210 -0
- package/my-test-app/src/controllers/AuthController.js +81 -0
- package/my-test-app/src/controllers/HomeController.js +17 -0
- package/my-test-app/src/controllers/PostController.js +86 -0
- package/my-test-app/src/controllers/ProfileController.js +66 -0
- package/my-test-app/src/helpers/api.js +24 -0
- package/my-test-app/src/main.jsx +9 -0
- package/my-test-app/src/middleware/auth.js +16 -0
- package/my-test-app/src/models/Post.js +63 -0
- package/my-test-app/src/models/User.js +42 -0
- package/my-test-app/src/routes/App.jsx +38 -0
- package/my-test-app/src/routes/api/auth.js +7 -0
- package/my-test-app/src/routes/api/posts.js +15 -0
- package/my-test-app/src/routes/api/profile.js +8 -0
- package/my-test-app/src/server/index.js +16 -0
- package/my-test-app/vite.config.js +18 -0
- package/package.json +61 -0
- package/scripts/test-app.sh +59 -0
- package/src/auth/jwt.js +195 -0
- package/src/auth/password.js +132 -0
- package/src/cli/colors.js +31 -0
- package/src/cli/dispatcher.js +168 -0
- package/src/cli/parser.js +91 -0
- package/src/client/context.js +4 -0
- package/src/client/hooks.js +50 -0
- package/src/client/index.js +3 -0
- package/src/client/router.js +184 -0
- package/src/commands/build.js +155 -0
- package/src/commands/dev.js +206 -0
- package/src/commands/help.js +84 -0
- package/src/commands/make-controller.js +36 -0
- package/src/commands/make-middleware.js +44 -0
- package/src/commands/make-migration.js +51 -0
- package/src/commands/make-model.js +38 -0
- package/src/commands/make-route.js +36 -0
- package/src/commands/make-seed.js +38 -0
- package/src/commands/migrate-fresh.js +32 -0
- package/src/commands/migrate-rollback.js +30 -0
- package/src/commands/migrate-status.js +41 -0
- package/src/commands/migrate.js +30 -0
- package/src/commands/seed.js +47 -0
- package/src/commands/start.js +69 -0
- package/src/commands/test.js +46 -0
- package/src/db/Grammar.js +125 -0
- package/src/db/QueryBuilder.js +476 -0
- package/src/db/adapters/neon.js +170 -0
- package/src/db/adapters/planetscale.js +146 -0
- package/src/db/adapters/postgres.js +166 -0
- package/src/db/adapters/sqlite.js +125 -0
- package/src/db/adapters/turso.js +165 -0
- package/src/db/index.js +156 -0
- package/src/db/migrator.js +250 -0
- package/src/db/seeder.js +124 -0
- package/src/index.js +12 -0
- package/src/scaffolding/index.js +152 -0
- package/src/server/body-parser.js +159 -0
- package/src/server/cors.js +63 -0
- package/src/server/default-entry.js +13 -0
- package/src/server/http.js +221 -0
- package/src/server/index.js +168 -0
- package/src/server/loader.js +128 -0
- package/src/server/router.js +281 -0
- package/src/server/static.js +139 -0
- package/src/validation/index.js +436 -0
- package/src/vite/config.js +49 -0
- package/stubs/controller.stub +48 -0
- package/stubs/middleware-auth.stub +29 -0
- package/stubs/middleware.stub +9 -0
- package/stubs/migration.stub +17 -0
- package/stubs/model.stub +77 -0
- package/stubs/route.stub +13 -0
- package/stubs/seed.stub +16 -0
- package/stubs/vite.config.stub +18 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useTheme } from '../components/ThemeContext'
|
|
2
|
+
import { Card } from '../components/Card'
|
|
3
|
+
import { PageHeader } from '../components/PageHeader'
|
|
4
|
+
import { AppLayout } from '../layouts/AppLayout'
|
|
5
|
+
import { DocsLayout } from '../layouts/DocsLayout'
|
|
6
|
+
|
|
7
|
+
export function Authentication() {
|
|
8
|
+
const { t } = useTheme()
|
|
9
|
+
|
|
10
|
+
const CodeBlock = ({ children, title }) => (
|
|
11
|
+
<div className="mt-4">
|
|
12
|
+
{title && <div className={`text-xs font-medium mb-2 ${t.muted}`}>{title}</div>}
|
|
13
|
+
<div className={`rounded-lg p-4 font-mono text-sm ${t.card} border ${t.border} overflow-x-auto`}>
|
|
14
|
+
<pre className={t.text}>{children}</pre>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<PageHeader
|
|
22
|
+
title="Authentication"
|
|
23
|
+
subtitle="JWT auth, password hashing, and protected routes"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<div className="space-y-6">
|
|
27
|
+
<Card>
|
|
28
|
+
<h2 className="text-lg font-semibold mb-2">Overview</h2>
|
|
29
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
30
|
+
BasicBen includes a complete JWT-based authentication system with password hashing, token generation, and middleware for protected routes.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
34
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
35
|
+
<div className="font-semibold text-sm">JWT Tokens</div>
|
|
36
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Stateless authentication</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
39
|
+
<div className="font-semibold text-sm">Bcrypt Hashing</div>
|
|
40
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Secure password storage</p>
|
|
41
|
+
</div>
|
|
42
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
43
|
+
<div className="font-semibold text-sm">Auth Middleware</div>
|
|
44
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Protect your routes</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</Card>
|
|
48
|
+
|
|
49
|
+
<Card>
|
|
50
|
+
<h2 className="text-lg font-semibold mb-2">Password Hashing</h2>
|
|
51
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
52
|
+
Use bcrypt to securely hash and verify passwords.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<CodeBlock title="Hash and verify passwords">
|
|
56
|
+
{`import { hashPassword, verifyPassword } from 'basicben/auth'
|
|
57
|
+
|
|
58
|
+
// Hash a password (for registration)
|
|
59
|
+
const hashedPassword = await hashPassword('user-password')
|
|
60
|
+
|
|
61
|
+
// Verify a password (for login)
|
|
62
|
+
const isValid = await verifyPassword('user-password', hashedPassword)
|
|
63
|
+
// Returns true or false`}
|
|
64
|
+
</CodeBlock>
|
|
65
|
+
</Card>
|
|
66
|
+
|
|
67
|
+
<Card>
|
|
68
|
+
<h2 className="text-lg font-semibold mb-2">JWT Tokens</h2>
|
|
69
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
70
|
+
Generate and verify JWT tokens for stateless authentication.
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
<CodeBlock title="Generate and verify tokens">
|
|
74
|
+
{`import { generateToken, verifyToken } from 'basicben/auth'
|
|
75
|
+
|
|
76
|
+
// Generate a token (after successful login)
|
|
77
|
+
const token = await generateToken({ userId: user.id })
|
|
78
|
+
|
|
79
|
+
// Verify a token (in middleware)
|
|
80
|
+
try {
|
|
81
|
+
const payload = await verifyToken(token)
|
|
82
|
+
console.log(payload.userId) // The user ID from the token
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Token is invalid or expired
|
|
85
|
+
}`}
|
|
86
|
+
</CodeBlock>
|
|
87
|
+
|
|
88
|
+
<CodeBlock title="Configure token expiration in basicben.config.js">
|
|
89
|
+
{`export default {
|
|
90
|
+
auth: {
|
|
91
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
92
|
+
jwtExpiresIn: '7d' // Token expires in 7 days
|
|
93
|
+
}
|
|
94
|
+
}`}
|
|
95
|
+
</CodeBlock>
|
|
96
|
+
</Card>
|
|
97
|
+
|
|
98
|
+
<Card>
|
|
99
|
+
<h2 className="text-lg font-semibold mb-2">Registration</h2>
|
|
100
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
101
|
+
Example registration endpoint that creates a user and returns a token.
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
<CodeBlock title="src/controllers/AuthController.js">
|
|
105
|
+
{`import { db } from 'basicben'
|
|
106
|
+
import { hashPassword, generateToken } from 'basicben/auth'
|
|
107
|
+
import { validate, rules } from 'basicben/validation'
|
|
108
|
+
|
|
109
|
+
export const AuthController = {
|
|
110
|
+
register: async (req, res) => {
|
|
111
|
+
// Validate input
|
|
112
|
+
const result = await validate(req.body, {
|
|
113
|
+
name: [rules.required, rules.minLength(2)],
|
|
114
|
+
email: [rules.required, rules.email, rules.unique('users')],
|
|
115
|
+
password: [rules.required, rules.minLength(8)]
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (!result.valid) {
|
|
119
|
+
return res.status(400).json({ errors: result.errors })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create user
|
|
123
|
+
const hashedPassword = await hashPassword(req.body.password)
|
|
124
|
+
|
|
125
|
+
const { lastInsertRowid } = await (await db.table('users')).insert({
|
|
126
|
+
name: req.body.name,
|
|
127
|
+
email: req.body.email,
|
|
128
|
+
password: hashedPassword
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Get the created user
|
|
132
|
+
const user = await (await db.table('users')).find(lastInsertRowid)
|
|
133
|
+
|
|
134
|
+
// Generate token
|
|
135
|
+
const token = await generateToken({ userId: user.id })
|
|
136
|
+
|
|
137
|
+
return res.status(201).json({
|
|
138
|
+
user: { id: user.id, name: user.name, email: user.email },
|
|
139
|
+
token
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}`}
|
|
143
|
+
</CodeBlock>
|
|
144
|
+
</Card>
|
|
145
|
+
|
|
146
|
+
<Card>
|
|
147
|
+
<h2 className="text-lg font-semibold mb-2">Login</h2>
|
|
148
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
149
|
+
Example login endpoint that verifies credentials and returns a token.
|
|
150
|
+
</p>
|
|
151
|
+
|
|
152
|
+
<CodeBlock title="Login handler">
|
|
153
|
+
{`import { verifyPassword, generateToken } from 'basicben/auth'
|
|
154
|
+
|
|
155
|
+
export const AuthController = {
|
|
156
|
+
login: async (req, res) => {
|
|
157
|
+
const { email, password } = req.body
|
|
158
|
+
|
|
159
|
+
// Find user by email
|
|
160
|
+
const user = await (await db.table('users'))
|
|
161
|
+
.where('email', email)
|
|
162
|
+
.first()
|
|
163
|
+
|
|
164
|
+
if (!user) {
|
|
165
|
+
return res.status(401).json({ error: 'Invalid credentials' })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Verify password
|
|
169
|
+
const valid = await verifyPassword(password, user.password)
|
|
170
|
+
|
|
171
|
+
if (!valid) {
|
|
172
|
+
return res.status(401).json({ error: 'Invalid credentials' })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Generate token
|
|
176
|
+
const token = await generateToken({ userId: user.id })
|
|
177
|
+
|
|
178
|
+
return res.json({
|
|
179
|
+
user: { id: user.id, name: user.name, email: user.email },
|
|
180
|
+
token
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}`}
|
|
184
|
+
</CodeBlock>
|
|
185
|
+
</Card>
|
|
186
|
+
|
|
187
|
+
<Card>
|
|
188
|
+
<h2 className="text-lg font-semibold mb-2">Auth Middleware</h2>
|
|
189
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
190
|
+
Protect routes by requiring a valid JWT token.
|
|
191
|
+
</p>
|
|
192
|
+
|
|
193
|
+
<CodeBlock title="src/middleware/auth.js">
|
|
194
|
+
{`import { verifyToken } from 'basicben/auth'
|
|
195
|
+
import { db } from 'basicben'
|
|
196
|
+
|
|
197
|
+
export const auth = async (req, res, next) => {
|
|
198
|
+
const header = req.headers.authorization
|
|
199
|
+
|
|
200
|
+
if (!header?.startsWith('Bearer ')) {
|
|
201
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const token = header.slice(7)
|
|
206
|
+
const { userId } = await verifyToken(token)
|
|
207
|
+
|
|
208
|
+
req.user = await (await db.table('users')).find(userId)
|
|
209
|
+
|
|
210
|
+
if (!req.user) {
|
|
211
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
next()
|
|
215
|
+
} catch {
|
|
216
|
+
return res.status(401).json({ error: 'Invalid token' })
|
|
217
|
+
}
|
|
218
|
+
}`}
|
|
219
|
+
</CodeBlock>
|
|
220
|
+
|
|
221
|
+
<CodeBlock title="Using auth middleware in routes">
|
|
222
|
+
{`import { auth } from '../middleware/auth.js'
|
|
223
|
+
|
|
224
|
+
export default [
|
|
225
|
+
// Public route
|
|
226
|
+
{ method: 'GET', path: '/api/posts', handler: PostController.index },
|
|
227
|
+
|
|
228
|
+
// Protected route - requires authentication
|
|
229
|
+
{ method: 'POST', path: '/api/posts', handler: PostController.store, middleware: [auth] },
|
|
230
|
+
]`}
|
|
231
|
+
</CodeBlock>
|
|
232
|
+
</Card>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { useTheme } from '../components/ThemeContext'
|
|
2
|
+
import { Card } from '../components/Card'
|
|
3
|
+
import { PageHeader } from '../components/PageHeader'
|
|
4
|
+
import { AppLayout } from '../layouts/AppLayout'
|
|
5
|
+
import { DocsLayout } from '../layouts/DocsLayout'
|
|
6
|
+
|
|
7
|
+
export function Database() {
|
|
8
|
+
const { t } = useTheme()
|
|
9
|
+
|
|
10
|
+
const CodeBlock = ({ children, title }) => (
|
|
11
|
+
<div className="mt-4">
|
|
12
|
+
{title && <div className={`text-xs font-medium mb-2 ${t.muted}`}>{title}</div>}
|
|
13
|
+
<div className={`rounded-lg p-4 font-mono text-sm ${t.card} border ${t.border} overflow-x-auto`}>
|
|
14
|
+
<pre className={t.text}>{children}</pre>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<PageHeader
|
|
22
|
+
title="Database"
|
|
23
|
+
subtitle="Adapters, migrations, seeding, and queries"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<div className="space-y-6">
|
|
27
|
+
{/* Database Adapters */}
|
|
28
|
+
<Card>
|
|
29
|
+
<h2 className="text-lg font-semibold mb-2">Database Adapters</h2>
|
|
30
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
31
|
+
BasicBen supports multiple databases. Configure your adapter in <code>basicben.config.js</code>.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 mb-4">
|
|
35
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
36
|
+
<div className="font-semibold text-sm">SQLite</div>
|
|
37
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Local file database (default)</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
40
|
+
<div className="font-semibold text-sm">PostgreSQL</div>
|
|
41
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Traditional Postgres</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
44
|
+
<div className="font-semibold text-sm">Turso</div>
|
|
45
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Edge SQLite (libSQL)</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
48
|
+
<div className="font-semibold text-sm">PlanetScale</div>
|
|
49
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Serverless MySQL</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
52
|
+
<div className="font-semibold text-sm">Neon</div>
|
|
53
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Serverless Postgres</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<CodeBlock title="basicben.config.js - SQLite (default)">
|
|
58
|
+
{`export default {
|
|
59
|
+
db: {
|
|
60
|
+
driver: 'sqlite',
|
|
61
|
+
url: './database.sqlite'
|
|
62
|
+
}
|
|
63
|
+
}`}
|
|
64
|
+
</CodeBlock>
|
|
65
|
+
|
|
66
|
+
<CodeBlock title="basicben.config.js - Turso">
|
|
67
|
+
{`export default {
|
|
68
|
+
db: {
|
|
69
|
+
driver: 'turso',
|
|
70
|
+
url: process.env.TURSO_URL,
|
|
71
|
+
authToken: process.env.TURSO_AUTH_TOKEN
|
|
72
|
+
}
|
|
73
|
+
}`}
|
|
74
|
+
</CodeBlock>
|
|
75
|
+
|
|
76
|
+
<CodeBlock title="basicben.config.js - PlanetScale">
|
|
77
|
+
{`export default {
|
|
78
|
+
db: {
|
|
79
|
+
driver: 'planetscale',
|
|
80
|
+
url: process.env.DATABASE_URL
|
|
81
|
+
}
|
|
82
|
+
}`}
|
|
83
|
+
</CodeBlock>
|
|
84
|
+
|
|
85
|
+
<CodeBlock title="basicben.config.js - Neon">
|
|
86
|
+
{`export default {
|
|
87
|
+
db: {
|
|
88
|
+
driver: 'neon',
|
|
89
|
+
url: process.env.DATABASE_URL
|
|
90
|
+
}
|
|
91
|
+
}`}
|
|
92
|
+
</CodeBlock>
|
|
93
|
+
|
|
94
|
+
<CodeBlock title="Install the driver you need">
|
|
95
|
+
{`# SQLite (default)
|
|
96
|
+
npm install better-sqlite3
|
|
97
|
+
|
|
98
|
+
# PostgreSQL
|
|
99
|
+
npm install pg
|
|
100
|
+
|
|
101
|
+
# Turso
|
|
102
|
+
npm install @libsql/client
|
|
103
|
+
|
|
104
|
+
# PlanetScale
|
|
105
|
+
npm install @planetscale/database
|
|
106
|
+
|
|
107
|
+
# Neon
|
|
108
|
+
npm install @neondatabase/serverless`}
|
|
109
|
+
</CodeBlock>
|
|
110
|
+
</Card>
|
|
111
|
+
|
|
112
|
+
{/* Migrations */}
|
|
113
|
+
<Card>
|
|
114
|
+
<h2 className="text-lg font-semibold mb-2">Migrations</h2>
|
|
115
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
116
|
+
Migrations let you version control your database schema. Each migration has an <code>up</code> function to apply changes and a <code>down</code> function to reverse them.
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<CodeBlock title="Generate a migration">
|
|
120
|
+
{`npx basicben make:migration create_posts`}
|
|
121
|
+
</CodeBlock>
|
|
122
|
+
|
|
123
|
+
<CodeBlock title="migrations/001_create_posts.js">
|
|
124
|
+
{`export const up = (db) => {
|
|
125
|
+
db.exec(\`
|
|
126
|
+
CREATE TABLE posts (
|
|
127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
128
|
+
user_id INTEGER NOT NULL,
|
|
129
|
+
title TEXT NOT NULL,
|
|
130
|
+
content TEXT,
|
|
131
|
+
published INTEGER DEFAULT 0,
|
|
132
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
133
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
134
|
+
)
|
|
135
|
+
\`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const down = (db) => {
|
|
139
|
+
db.exec('DROP TABLE posts')
|
|
140
|
+
}`}
|
|
141
|
+
</CodeBlock>
|
|
142
|
+
|
|
143
|
+
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
|
144
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
145
|
+
<code className="text-sm font-semibold">npx basicben migrate</code>
|
|
146
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Run pending migrations</p>
|
|
147
|
+
</div>
|
|
148
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
149
|
+
<code className="text-sm font-semibold">npx basicben migrate:rollback</code>
|
|
150
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Undo last batch</p>
|
|
151
|
+
</div>
|
|
152
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
153
|
+
<code className="text-sm font-semibold">npx basicben migrate:fresh</code>
|
|
154
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Drop all & re-run</p>
|
|
155
|
+
</div>
|
|
156
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
157
|
+
<code className="text-sm font-semibold">npx basicben migrate:status</code>
|
|
158
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Show migration status</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
{/* Seeding */}
|
|
164
|
+
<Card>
|
|
165
|
+
<h2 className="text-lg font-semibold mb-2">Seeding</h2>
|
|
166
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
167
|
+
Seeds populate your database with initial or test data. Create seed files in the <code>seeds/</code> directory. Seeds run in alphabetical order.
|
|
168
|
+
</p>
|
|
169
|
+
|
|
170
|
+
<CodeBlock title="Generate a seed file">
|
|
171
|
+
{`npx basicben make:seed users
|
|
172
|
+
# Creates: seeds/users.js`}
|
|
173
|
+
</CodeBlock>
|
|
174
|
+
|
|
175
|
+
<CodeBlock title="seeds/01_users.js">
|
|
176
|
+
{`import { db } from 'basicben'
|
|
177
|
+
import { hashPassword } from 'basicben/auth'
|
|
178
|
+
|
|
179
|
+
export async function seed() {
|
|
180
|
+
const password = await hashPassword('password123')
|
|
181
|
+
|
|
182
|
+
await (await db.table('users')).insert({
|
|
183
|
+
name: 'Admin User',
|
|
184
|
+
email: 'admin@example.com',
|
|
185
|
+
password
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
await (await db.table('users')).insert({
|
|
189
|
+
name: 'Test User',
|
|
190
|
+
email: 'test@example.com',
|
|
191
|
+
password
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
console.log('Seeded 2 users')
|
|
195
|
+
}`}
|
|
196
|
+
</CodeBlock>
|
|
197
|
+
|
|
198
|
+
<CodeBlock title="seeds/02_posts.js">
|
|
199
|
+
{`import { db } from 'basicben'
|
|
200
|
+
|
|
201
|
+
export async function seed() {
|
|
202
|
+
const user = await (await db.table('users')).first()
|
|
203
|
+
|
|
204
|
+
await (await db.table('posts')).insert({
|
|
205
|
+
user_id: user.id,
|
|
206
|
+
title: 'Welcome to BasicBen',
|
|
207
|
+
content: 'Your first blog post!',
|
|
208
|
+
published: 1
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
console.log('Seeded posts')
|
|
212
|
+
}`}
|
|
213
|
+
</CodeBlock>
|
|
214
|
+
|
|
215
|
+
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
|
216
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
217
|
+
<code className="text-sm font-semibold">npx basicben seed</code>
|
|
218
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Run all seeds</p>
|
|
219
|
+
</div>
|
|
220
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
221
|
+
<code className="text-sm font-semibold">npx basicben seed users</code>
|
|
222
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Run specific seed</p>
|
|
223
|
+
</div>
|
|
224
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
225
|
+
<code className="text-sm font-semibold">npx basicben make:seed</code>
|
|
226
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Generate seed file</p>
|
|
227
|
+
</div>
|
|
228
|
+
<div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
|
|
229
|
+
<code className="text-sm font-semibold">npx basicben db:seed</code>
|
|
230
|
+
<p className={`text-xs mt-1 ${t.muted}`}>Alias for seed</p>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{/* Query Builder */}
|
|
236
|
+
<Card>
|
|
237
|
+
<h2 className="text-lg font-semibold mb-2">Query Builder</h2>
|
|
238
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
239
|
+
The query builder provides a fluent API for database queries with built-in SQL injection protection.
|
|
240
|
+
</p>
|
|
241
|
+
|
|
242
|
+
<CodeBlock title="Basic queries">
|
|
243
|
+
{`import { db } from 'basicben'
|
|
244
|
+
|
|
245
|
+
// Get all users
|
|
246
|
+
const users = await (await db.table('users')).get()
|
|
247
|
+
|
|
248
|
+
// Find by ID
|
|
249
|
+
const user = await (await db.table('users')).find(1)
|
|
250
|
+
|
|
251
|
+
// Filter with where
|
|
252
|
+
const admins = await (await db.table('users'))
|
|
253
|
+
.where('is_admin', true)
|
|
254
|
+
.get()
|
|
255
|
+
|
|
256
|
+
// Chain multiple conditions
|
|
257
|
+
const results = await (await db.table('posts'))
|
|
258
|
+
.where('published', true)
|
|
259
|
+
.where('user_id', userId)
|
|
260
|
+
.orderBy('created_at', 'DESC')
|
|
261
|
+
.limit(10)
|
|
262
|
+
.get()`}
|
|
263
|
+
</CodeBlock>
|
|
264
|
+
|
|
265
|
+
<CodeBlock title="Insert & Update">
|
|
266
|
+
{`// Insert a record
|
|
267
|
+
const result = await (await db.table('posts'))
|
|
268
|
+
.insert({
|
|
269
|
+
title: 'My Post',
|
|
270
|
+
content: 'Hello world',
|
|
271
|
+
user_id: 1
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
console.log(result.lastInsertRowid) // New ID
|
|
275
|
+
|
|
276
|
+
// Update records
|
|
277
|
+
await (await db.table('posts'))
|
|
278
|
+
.where('id', postId)
|
|
279
|
+
.update({ title: 'Updated Title' })
|
|
280
|
+
|
|
281
|
+
// Delete records
|
|
282
|
+
await (await db.table('posts'))
|
|
283
|
+
.where('id', postId)
|
|
284
|
+
.delete()`}
|
|
285
|
+
</CodeBlock>
|
|
286
|
+
|
|
287
|
+
<CodeBlock title="Aggregates & Pagination">
|
|
288
|
+
{`// Count records
|
|
289
|
+
const count = await (await db.table('posts'))
|
|
290
|
+
.where('published', true)
|
|
291
|
+
.count()
|
|
292
|
+
|
|
293
|
+
// Check existence
|
|
294
|
+
const exists = await (await db.table('users'))
|
|
295
|
+
.where('email', 'test@example.com')
|
|
296
|
+
.exists()
|
|
297
|
+
|
|
298
|
+
// Paginate results
|
|
299
|
+
const page = await (await db.table('posts'))
|
|
300
|
+
.orderBy('created_at', 'DESC')
|
|
301
|
+
.paginate(1, 15)
|
|
302
|
+
|
|
303
|
+
// Returns: { data, total, page, perPage, totalPages }`}
|
|
304
|
+
</CodeBlock>
|
|
305
|
+
</Card>
|
|
306
|
+
|
|
307
|
+
{/* Mass Assignment Protection */}
|
|
308
|
+
<Card>
|
|
309
|
+
<h2 className="text-lg font-semibold mb-2">Mass Assignment Protection</h2>
|
|
310
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
311
|
+
Prevent users from setting fields they shouldn't (like <code>is_admin</code>) by using <code>only()</code> or <code>except()</code>.
|
|
312
|
+
</p>
|
|
313
|
+
|
|
314
|
+
<CodeBlock title="Whitelist with only()">
|
|
315
|
+
{`// Only allow these fields to be set
|
|
316
|
+
await (await db.table('users'))
|
|
317
|
+
.only('name', 'email', 'bio')
|
|
318
|
+
.insert(req.body)
|
|
319
|
+
|
|
320
|
+
// Extra fields in req.body are ignored
|
|
321
|
+
// { name: 'Bob', email: 'bob@test.com', is_admin: true }
|
|
322
|
+
// is_admin is silently dropped`}
|
|
323
|
+
</CodeBlock>
|
|
324
|
+
|
|
325
|
+
<CodeBlock title="Blacklist with except()">
|
|
326
|
+
{`// Block specific fields
|
|
327
|
+
await (await db.table('users'))
|
|
328
|
+
.except('id', 'is_admin', 'created_at')
|
|
329
|
+
.where('id', userId)
|
|
330
|
+
.update(req.body)`}
|
|
331
|
+
</CodeBlock>
|
|
332
|
+
|
|
333
|
+
<CodeBlock title="In your models">
|
|
334
|
+
{`// src/models/User.js
|
|
335
|
+
export const User = {
|
|
336
|
+
fillable: ['name', 'email', 'bio'],
|
|
337
|
+
|
|
338
|
+
create: async (data) => {
|
|
339
|
+
return (await db.table('users'))
|
|
340
|
+
.only(...User.fillable)
|
|
341
|
+
.insert(data)
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
update: async (id, data) => {
|
|
345
|
+
return (await db.table('users'))
|
|
346
|
+
.only(...User.fillable)
|
|
347
|
+
.where('id', id)
|
|
348
|
+
.update(data)
|
|
349
|
+
}
|
|
350
|
+
}`}
|
|
351
|
+
</CodeBlock>
|
|
352
|
+
</Card>
|
|
353
|
+
|
|
354
|
+
{/* Database Validation */}
|
|
355
|
+
<Card>
|
|
356
|
+
<h2 className="text-lg font-semibold mb-2">Database Validation</h2>
|
|
357
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
358
|
+
Validate data against your database with <code>unique</code> and <code>exists</code> rules.
|
|
359
|
+
</p>
|
|
360
|
+
|
|
361
|
+
<CodeBlock title="Unique validation">
|
|
362
|
+
{`import { validate, rules } from 'basicben/validation'
|
|
363
|
+
|
|
364
|
+
// Check email is unique in users table
|
|
365
|
+
const result = await validate(req.body, {
|
|
366
|
+
email: [rules.required, rules.email, rules.unique('users')]
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// With custom column name
|
|
370
|
+
slug: [rules.unique('categories', 'slug')]
|
|
371
|
+
|
|
372
|
+
// Exclude current record (for updates)
|
|
373
|
+
email: [rules.unique('users', 'email', userId)]`}
|
|
374
|
+
</CodeBlock>
|
|
375
|
+
|
|
376
|
+
<CodeBlock title="Exists validation">
|
|
377
|
+
{`// Check foreign key exists
|
|
378
|
+
const result = await validate(req.body, {
|
|
379
|
+
user_id: [rules.required, rules.exists('users')],
|
|
380
|
+
category_id: [rules.required, rules.exists('categories')]
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// With custom column
|
|
384
|
+
category: [rules.exists('categories', 'slug')]`}
|
|
385
|
+
</CodeBlock>
|
|
386
|
+
</Card>
|
|
387
|
+
|
|
388
|
+
{/* Raw Queries */}
|
|
389
|
+
<Card>
|
|
390
|
+
<h2 className="text-lg font-semibold mb-2">Raw Queries</h2>
|
|
391
|
+
<p className={`text-sm ${t.muted} mb-4`}>
|
|
392
|
+
For complex queries, you can use raw SQL with parameterized queries.
|
|
393
|
+
</p>
|
|
394
|
+
|
|
395
|
+
<CodeBlock title="Using db directly">
|
|
396
|
+
{`import { db } from 'basicben'
|
|
397
|
+
|
|
398
|
+
// Parameterized queries (safe from SQL injection)
|
|
399
|
+
const posts = await db.all(
|
|
400
|
+
'SELECT * FROM posts WHERE user_id = ? AND published = ?',
|
|
401
|
+
[userId, true]
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
// Single row
|
|
405
|
+
const user = await db.get(
|
|
406
|
+
'SELECT * FROM users WHERE email = ?',
|
|
407
|
+
[email]
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
// Insert/Update/Delete
|
|
411
|
+
const result = await db.run(
|
|
412
|
+
'INSERT INTO posts (title, user_id) VALUES (?, ?)',
|
|
413
|
+
[title, userId]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
// Transactions
|
|
417
|
+
await db.transaction(async (tx) => {
|
|
418
|
+
await tx.run('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, fromId])
|
|
419
|
+
await tx.run('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, toId])
|
|
420
|
+
})`}
|
|
421
|
+
</CodeBlock>
|
|
422
|
+
</Card>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from '@basicbenframework/core/client'
|
|
3
|
+
import { PageHeader } from '../components/PageHeader'
|
|
4
|
+
import { PostCard } from '../components/PostCard'
|
|
5
|
+
import { Loading } from '../components/Loading'
|
|
6
|
+
import { Empty } from '../components/Empty'
|
|
7
|
+
import { api } from '../../helpers/api'
|
|
8
|
+
|
|
9
|
+
export function Feed() {
|
|
10
|
+
const navigate = useNavigate()
|
|
11
|
+
const [posts, setPosts] = useState([])
|
|
12
|
+
const [loading, setLoading] = useState(true)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
api('/api/feed').then(data => setPosts(data.posts)).finally(() => setLoading(false))
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
if (loading) return <Loading />
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<PageHeader title="Feed" />
|
|
23
|
+
{posts.length === 0 ? (
|
|
24
|
+
<Empty>No posts yet</Empty>
|
|
25
|
+
) : (
|
|
26
|
+
<div className="space-y-4">
|
|
27
|
+
{posts.map(post => (
|
|
28
|
+
<PostCard key={post.id} post={post} onClick={() => navigate(`/feed/${post.id}`)} showAuthor />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|