@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.
Files changed (186) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/README.md +588 -0
  3. package/bin/cli.js +8 -0
  4. package/create-basicben-app/index.js +205 -0
  5. package/create-basicben-app/package.json +30 -0
  6. package/create-basicben-app/template/.env.example +24 -0
  7. package/create-basicben-app/template/README.md +59 -0
  8. package/create-basicben-app/template/basicben.config.js +33 -0
  9. package/create-basicben-app/template/index.html +54 -0
  10. package/create-basicben-app/template/migrations/001_create_users.js +15 -0
  11. package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
  12. package/create-basicben-app/template/public/.gitkeep +0 -0
  13. package/create-basicben-app/template/seeds/01_users.js +29 -0
  14. package/create-basicben-app/template/seeds/02_posts.js +43 -0
  15. package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
  16. package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
  17. package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
  18. package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
  19. package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
  20. package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
  21. package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
  22. package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
  23. package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
  24. package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  25. package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
  26. package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
  27. package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
  28. package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
  29. package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
  30. package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
  31. package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
  32. package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
  33. package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
  34. package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
  35. package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
  36. package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
  37. package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
  38. package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
  39. package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
  40. package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
  41. package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
  42. package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
  43. package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
  44. package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
  45. package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
  46. package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
  47. package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
  48. package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
  49. package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
  50. package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
  51. package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
  52. package/create-basicben-app/template/src/controllers/PostController.js +86 -0
  53. package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
  54. package/create-basicben-app/template/src/helpers/api.js +24 -0
  55. package/create-basicben-app/template/src/main.jsx +9 -0
  56. package/create-basicben-app/template/src/middleware/auth.js +16 -0
  57. package/create-basicben-app/template/src/models/Post.js +63 -0
  58. package/create-basicben-app/template/src/models/User.js +42 -0
  59. package/create-basicben-app/template/src/routes/App.jsx +38 -0
  60. package/create-basicben-app/template/src/routes/api/auth.js +7 -0
  61. package/create-basicben-app/template/src/routes/api/posts.js +15 -0
  62. package/create-basicben-app/template/src/routes/api/profile.js +8 -0
  63. package/create-basicben-app/template/src/server/index.js +16 -0
  64. package/create-basicben-app/template/vite.config.js +18 -0
  65. package/database.sqlite +0 -0
  66. package/my-test-app/.env.example +24 -0
  67. package/my-test-app/README.md +59 -0
  68. package/my-test-app/basicben.config.js +33 -0
  69. package/my-test-app/database.sqlite-shm +0 -0
  70. package/my-test-app/database.sqlite-wal +0 -0
  71. package/my-test-app/index.html +54 -0
  72. package/my-test-app/migrations/001_create_users.js +15 -0
  73. package/my-test-app/migrations/002_create_posts.js +18 -0
  74. package/my-test-app/package-lock.json +2160 -0
  75. package/my-test-app/package.json +29 -0
  76. package/my-test-app/public/.gitkeep +0 -0
  77. package/my-test-app/seeds/01_users.js +29 -0
  78. package/my-test-app/seeds/02_posts.js +43 -0
  79. package/my-test-app/src/client/components/Alert.jsx +11 -0
  80. package/my-test-app/src/client/components/Avatar.jsx +11 -0
  81. package/my-test-app/src/client/components/BackLink.jsx +10 -0
  82. package/my-test-app/src/client/components/Button.jsx +19 -0
  83. package/my-test-app/src/client/components/Card.jsx +10 -0
  84. package/my-test-app/src/client/components/Empty.jsx +6 -0
  85. package/my-test-app/src/client/components/Input.jsx +12 -0
  86. package/my-test-app/src/client/components/Loading.jsx +6 -0
  87. package/my-test-app/src/client/components/Logo.jsx +40 -0
  88. package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  89. package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
  90. package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
  91. package/my-test-app/src/client/components/NavLink.jsx +10 -0
  92. package/my-test-app/src/client/components/PageHeader.jsx +8 -0
  93. package/my-test-app/src/client/components/PostCard.jsx +19 -0
  94. package/my-test-app/src/client/components/Textarea.jsx +12 -0
  95. package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
  96. package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
  97. package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
  98. package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
  99. package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
  100. package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
  101. package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
  102. package/my-test-app/src/client/pages/Auth.jsx +55 -0
  103. package/my-test-app/src/client/pages/Authentication.jsx +236 -0
  104. package/my-test-app/src/client/pages/Database.jsx +426 -0
  105. package/my-test-app/src/client/pages/Feed.jsx +34 -0
  106. package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
  107. package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
  108. package/my-test-app/src/client/pages/Home.jsx +206 -0
  109. package/my-test-app/src/client/pages/PostForm.jsx +69 -0
  110. package/my-test-app/src/client/pages/Posts.jsx +59 -0
  111. package/my-test-app/src/client/pages/Profile.jsx +68 -0
  112. package/my-test-app/src/client/pages/Routing.jsx +207 -0
  113. package/my-test-app/src/client/pages/Testing.jsx +251 -0
  114. package/my-test-app/src/client/pages/Validation.jsx +210 -0
  115. package/my-test-app/src/controllers/AuthController.js +81 -0
  116. package/my-test-app/src/controllers/HomeController.js +17 -0
  117. package/my-test-app/src/controllers/PostController.js +86 -0
  118. package/my-test-app/src/controllers/ProfileController.js +66 -0
  119. package/my-test-app/src/helpers/api.js +24 -0
  120. package/my-test-app/src/main.jsx +9 -0
  121. package/my-test-app/src/middleware/auth.js +16 -0
  122. package/my-test-app/src/models/Post.js +63 -0
  123. package/my-test-app/src/models/User.js +42 -0
  124. package/my-test-app/src/routes/App.jsx +38 -0
  125. package/my-test-app/src/routes/api/auth.js +7 -0
  126. package/my-test-app/src/routes/api/posts.js +15 -0
  127. package/my-test-app/src/routes/api/profile.js +8 -0
  128. package/my-test-app/src/server/index.js +16 -0
  129. package/my-test-app/vite.config.js +18 -0
  130. package/package.json +61 -0
  131. package/scripts/test-app.sh +59 -0
  132. package/src/auth/jwt.js +195 -0
  133. package/src/auth/password.js +132 -0
  134. package/src/cli/colors.js +31 -0
  135. package/src/cli/dispatcher.js +168 -0
  136. package/src/cli/parser.js +91 -0
  137. package/src/client/context.js +4 -0
  138. package/src/client/hooks.js +50 -0
  139. package/src/client/index.js +3 -0
  140. package/src/client/router.js +184 -0
  141. package/src/commands/build.js +155 -0
  142. package/src/commands/dev.js +206 -0
  143. package/src/commands/help.js +84 -0
  144. package/src/commands/make-controller.js +36 -0
  145. package/src/commands/make-middleware.js +44 -0
  146. package/src/commands/make-migration.js +51 -0
  147. package/src/commands/make-model.js +38 -0
  148. package/src/commands/make-route.js +36 -0
  149. package/src/commands/make-seed.js +38 -0
  150. package/src/commands/migrate-fresh.js +32 -0
  151. package/src/commands/migrate-rollback.js +30 -0
  152. package/src/commands/migrate-status.js +41 -0
  153. package/src/commands/migrate.js +30 -0
  154. package/src/commands/seed.js +47 -0
  155. package/src/commands/start.js +69 -0
  156. package/src/commands/test.js +46 -0
  157. package/src/db/Grammar.js +125 -0
  158. package/src/db/QueryBuilder.js +476 -0
  159. package/src/db/adapters/neon.js +170 -0
  160. package/src/db/adapters/planetscale.js +146 -0
  161. package/src/db/adapters/postgres.js +166 -0
  162. package/src/db/adapters/sqlite.js +125 -0
  163. package/src/db/adapters/turso.js +165 -0
  164. package/src/db/index.js +156 -0
  165. package/src/db/migrator.js +250 -0
  166. package/src/db/seeder.js +124 -0
  167. package/src/index.js +12 -0
  168. package/src/scaffolding/index.js +152 -0
  169. package/src/server/body-parser.js +159 -0
  170. package/src/server/cors.js +63 -0
  171. package/src/server/default-entry.js +13 -0
  172. package/src/server/http.js +221 -0
  173. package/src/server/index.js +168 -0
  174. package/src/server/loader.js +128 -0
  175. package/src/server/router.js +281 -0
  176. package/src/server/static.js +139 -0
  177. package/src/validation/index.js +436 -0
  178. package/src/vite/config.js +49 -0
  179. package/stubs/controller.stub +48 -0
  180. package/stubs/middleware-auth.stub +29 -0
  181. package/stubs/middleware.stub +9 -0
  182. package/stubs/migration.stub +17 -0
  183. package/stubs/model.stub +77 -0
  184. package/stubs/route.stub +13 -0
  185. package/stubs/seed.stub +16 -0
  186. package/stubs/vite.config.stub +18 -0
@@ -0,0 +1,251 @@
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 Testing() {
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="Testing"
23
+ subtitle="Write and run tests with Vitest"
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 uses Vitest for fast, modern testing. Tests are automatically discovered in files ending with <code>.test.js</code> or <code>.spec.js</code>.
31
+ </p>
32
+
33
+ <div className="grid gap-3 sm:grid-cols-2">
34
+ <div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
35
+ <code className="text-sm font-semibold">npm run test</code>
36
+ <p className={`text-xs mt-1 ${t.muted}`}>Run tests once</p>
37
+ </div>
38
+ <div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
39
+ <code className="text-sm font-semibold">npm run test:watch</code>
40
+ <p className={`text-xs mt-1 ${t.muted}`}>Run tests in watch mode</p>
41
+ </div>
42
+ </div>
43
+ </Card>
44
+
45
+ <Card>
46
+ <h2 className="text-lg font-semibold mb-2">Writing Tests</h2>
47
+ <p className={`text-sm ${t.muted} mb-4`}>
48
+ Create test files alongside your code or in a <code>tests/</code> directory.
49
+ </p>
50
+
51
+ <CodeBlock title="src/utils/helpers.test.js">
52
+ {`import { describe, it, expect } from 'vitest'
53
+ import { formatDate, slugify } from './helpers'
54
+
55
+ describe('formatDate', () => {
56
+ it('formats a date correctly', () => {
57
+ const date = new Date('2024-01-15')
58
+ expect(formatDate(date)).toBe('January 15, 2024')
59
+ })
60
+
61
+ it('handles invalid dates', () => {
62
+ expect(formatDate(null)).toBe('')
63
+ })
64
+ })
65
+
66
+ describe('slugify', () => {
67
+ it('converts text to a slug', () => {
68
+ expect(slugify('Hello World')).toBe('hello-world')
69
+ expect(slugify('This is a TEST')).toBe('this-is-a-test')
70
+ })
71
+
72
+ it('removes special characters', () => {
73
+ expect(slugify('Hello! World?')).toBe('hello-world')
74
+ })
75
+ })`}
76
+ </CodeBlock>
77
+ </Card>
78
+
79
+ <Card>
80
+ <h2 className="text-lg font-semibold mb-2">Testing API Routes</h2>
81
+ <p className={`text-sm ${t.muted} mb-4`}>
82
+ Test your API endpoints using the built-in test client.
83
+ </p>
84
+
85
+ <CodeBlock title="tests/api/posts.test.js">
86
+ {`import { describe, it, expect, beforeEach } from 'vitest'
87
+ import { testClient, resetDatabase } from 'basicben/testing'
88
+
89
+ describe('POST /api/posts', () => {
90
+ beforeEach(async () => {
91
+ await resetDatabase()
92
+ })
93
+
94
+ it('creates a post when authenticated', async () => {
95
+ const { body, status } = await testClient
96
+ .post('/api/posts')
97
+ .auth(testUser)
98
+ .send({
99
+ title: 'Test Post',
100
+ content: 'This is a test'
101
+ })
102
+
103
+ expect(status).toBe(201)
104
+ expect(body.id).toBeDefined()
105
+ })
106
+
107
+ it('returns 401 when not authenticated', async () => {
108
+ const { status } = await testClient
109
+ .post('/api/posts')
110
+ .send({ title: 'Test' })
111
+
112
+ expect(status).toBe(401)
113
+ })
114
+
115
+ it('validates required fields', async () => {
116
+ const { body, status } = await testClient
117
+ .post('/api/posts')
118
+ .auth(testUser)
119
+ .send({})
120
+
121
+ expect(status).toBe(400)
122
+ expect(body.errors.title).toBeDefined()
123
+ })
124
+ })`}
125
+ </CodeBlock>
126
+ </Card>
127
+
128
+ <Card>
129
+ <h2 className="text-lg font-semibold mb-2">Test Database</h2>
130
+ <p className={`text-sm ${t.muted} mb-4`}>
131
+ Tests run against an isolated test database that resets between tests.
132
+ </p>
133
+
134
+ <CodeBlock title="Database helpers">
135
+ {`import { resetDatabase, seedDatabase, factory } from 'basicben/testing'
136
+
137
+ describe('User tests', () => {
138
+ beforeEach(async () => {
139
+ // Reset database to clean state
140
+ await resetDatabase()
141
+
142
+ // Optionally seed with test data
143
+ await seedDatabase()
144
+ })
145
+
146
+ it('can create users with factory', async () => {
147
+ // Create a user using the factory
148
+ const user = await factory.user.create({
149
+ name: 'Test User',
150
+ email: 'test@example.com'
151
+ })
152
+
153
+ expect(user.id).toBeDefined()
154
+ })
155
+
156
+ it('can create multiple records', async () => {
157
+ // Create 5 posts for a user
158
+ const posts = await factory.post.createMany(5, {
159
+ user_id: testUser.id
160
+ })
161
+
162
+ expect(posts).toHaveLength(5)
163
+ })
164
+ })`}
165
+ </CodeBlock>
166
+ </Card>
167
+
168
+ <Card>
169
+ <h2 className="text-lg font-semibold mb-2">Mocking</h2>
170
+ <p className={`text-sm ${t.muted} mb-4`}>
171
+ Use Vitest's built-in mocking for external dependencies.
172
+ </p>
173
+
174
+ <CodeBlock title="Mocking modules">
175
+ {`import { describe, it, expect, vi } from 'vitest'
176
+ import { sendEmail } from './email'
177
+
178
+ // Mock the email module
179
+ vi.mock('./email', () => ({
180
+ sendEmail: vi.fn()
181
+ }))
182
+
183
+ describe('Registration', () => {
184
+ it('sends welcome email after registration', async () => {
185
+ await testClient
186
+ .post('/api/auth/register')
187
+ .send({
188
+ name: 'New User',
189
+ email: 'new@example.com',
190
+ password: 'password123'
191
+ })
192
+
193
+ expect(sendEmail).toHaveBeenCalledWith(
194
+ 'new@example.com',
195
+ 'Welcome!',
196
+ expect.any(String)
197
+ )
198
+ })
199
+ })`}
200
+ </CodeBlock>
201
+ </Card>
202
+
203
+ <Card>
204
+ <h2 className="text-lg font-semibold mb-2">Code Coverage</h2>
205
+ <p className={`text-sm ${t.muted} mb-4`}>
206
+ Generate code coverage reports to see what's tested.
207
+ </p>
208
+
209
+ <div className="grid gap-3 sm:grid-cols-2">
210
+ <div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
211
+ <code className="text-sm font-semibold">npm run test:coverage</code>
212
+ <p className={`text-xs mt-1 ${t.muted}`}>Run tests with coverage</p>
213
+ </div>
214
+ <div className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
215
+ <code className="text-sm font-semibold">npm run test:ui</code>
216
+ <p className={`text-xs mt-1 ${t.muted}`}>Open Vitest UI</p>
217
+ </div>
218
+ </div>
219
+
220
+ <CodeBlock title="Coverage output">
221
+ {`----------|---------|----------|---------|---------|
222
+ File | % Stmts | % Branch | % Funcs | % Lines |
223
+ ----------|---------|----------|---------|---------|
224
+ All files | 85.71 | 78.26 | 90.00 | 85.71 |
225
+ auth.js | 100.00 | 100.00 | 100.00 | 100.00 |
226
+ posts.js | 75.00 | 66.67 | 80.00 | 75.00 |
227
+ ----------|---------|----------|---------|---------|`}
228
+ </CodeBlock>
229
+ </Card>
230
+
231
+ <Card>
232
+ <h2 className="text-lg font-semibold mb-2">Testing Best Practices</h2>
233
+ <div className="space-y-3 mt-4">
234
+ {[
235
+ { title: 'Isolate tests', desc: 'Each test should be independent and not rely on other tests' },
236
+ { title: 'Reset state', desc: 'Use beforeEach to reset the database and any mocks' },
237
+ { title: 'Test behavior', desc: 'Test what the code does, not how it does it' },
238
+ { title: 'Use descriptive names', desc: 'Test names should describe the expected behavior' },
239
+ { title: 'Keep tests fast', desc: 'Mock external services and use test databases' },
240
+ ].map(({ title, desc }) => (
241
+ <div key={title} className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
242
+ <div className="font-semibold text-sm">{title}</div>
243
+ <p className={`text-xs mt-1 ${t.muted}`}>{desc}</p>
244
+ </div>
245
+ ))}
246
+ </div>
247
+ </Card>
248
+ </div>
249
+ </div>
250
+ )
251
+ }
@@ -0,0 +1,210 @@
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 Validation() {
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="Validation"
23
+ subtitle="Request validation with built-in rules"
24
+ />
25
+
26
+ <div className="space-y-6">
27
+ <Card>
28
+ <h2 className="text-lg font-semibold mb-2">Basic Usage</h2>
29
+ <p className={`text-sm ${t.muted} mb-4`}>
30
+ Validate request data using the <code>validate</code> function and built-in rules.
31
+ </p>
32
+
33
+ <CodeBlock title="Validating request body">
34
+ {`import { validate, rules } from 'basicben/validation'
35
+
36
+ export const store = async (req, res) => {
37
+ const result = await validate(req.body, {
38
+ title: [rules.required, rules.minLength(3), rules.maxLength(100)],
39
+ email: [rules.required, rules.email],
40
+ age: [rules.required, rules.number, rules.min(18)]
41
+ })
42
+
43
+ if (!result.valid) {
44
+ return res.status(400).json({ errors: result.errors })
45
+ }
46
+
47
+ // Data is valid, continue...
48
+ }`}
49
+ </CodeBlock>
50
+
51
+ <CodeBlock title="Error response format">
52
+ {`{
53
+ "errors": {
54
+ "title": ["Title is required"],
55
+ "email": ["Invalid email format"],
56
+ "age": ["Must be at least 18"]
57
+ }
58
+ }`}
59
+ </CodeBlock>
60
+ </Card>
61
+
62
+ <Card>
63
+ <h2 className="text-lg font-semibold mb-2">Available Rules</h2>
64
+ <p className={`text-sm ${t.muted} mb-4`}>
65
+ BasicBen includes commonly used validation rules out of the box.
66
+ </p>
67
+
68
+ <div className="grid gap-2 sm:grid-cols-2">
69
+ {[
70
+ { rule: 'required', desc: 'Field must be present and not empty' },
71
+ { rule: 'email', desc: 'Must be a valid email address' },
72
+ { rule: 'number', desc: 'Must be a number' },
73
+ { rule: 'string', desc: 'Must be a string' },
74
+ { rule: 'boolean', desc: 'Must be true or false' },
75
+ { rule: 'array', desc: 'Must be an array' },
76
+ { rule: 'minLength(n)', desc: 'String must be at least n characters' },
77
+ { rule: 'maxLength(n)', desc: 'String must be at most n characters' },
78
+ { rule: 'min(n)', desc: 'Number must be at least n' },
79
+ { rule: 'max(n)', desc: 'Number must be at most n' },
80
+ { rule: 'regex(pattern)', desc: 'Must match the regex pattern' },
81
+ { rule: 'in(values)', desc: 'Must be one of the given values' },
82
+ ].map(({ rule, desc }) => (
83
+ <div key={rule} className={`rounded-lg p-3 ${t.card} border ${t.border}`}>
84
+ <code className="text-sm font-semibold">{rule}</code>
85
+ <p className={`text-xs mt-1 ${t.muted}`}>{desc}</p>
86
+ </div>
87
+ ))}
88
+ </div>
89
+ </Card>
90
+
91
+ <Card>
92
+ <h2 className="text-lg font-semibold mb-2">Database Rules</h2>
93
+ <p className={`text-sm ${t.muted} mb-4`}>
94
+ Validate against your database with <code>unique</code> and <code>exists</code> rules.
95
+ </p>
96
+
97
+ <CodeBlock title="Unique validation">
98
+ {`// Check if email is unique in users table
99
+ email: [rules.required, rules.email, rules.unique('users')]
100
+
101
+ // With custom column
102
+ slug: [rules.unique('categories', 'slug')]
103
+
104
+ // Exclude current record (for updates)
105
+ email: [rules.unique('users', 'email', currentUserId)]`}
106
+ </CodeBlock>
107
+
108
+ <CodeBlock title="Exists validation">
109
+ {`// Check if user_id exists in users table
110
+ user_id: [rules.required, rules.exists('users')]
111
+
112
+ // With custom column
113
+ category: [rules.exists('categories', 'slug')]`}
114
+ </CodeBlock>
115
+ </Card>
116
+
117
+ <Card>
118
+ <h2 className="text-lg font-semibold mb-2">Custom Rules</h2>
119
+ <p className={`text-sm ${t.muted} mb-4`}>
120
+ Create custom validation rules for specific requirements.
121
+ </p>
122
+
123
+ <CodeBlock title="Creating a custom rule">
124
+ {`// Custom rule as a function
125
+ const isSlug = (value, field) => {
126
+ if (!/^[a-z0-9-]+$/.test(value)) {
127
+ return \`\${field} must only contain lowercase letters, numbers, and hyphens\`
128
+ }
129
+ return null // Return null if valid
130
+ }
131
+
132
+ // Use it in validation
133
+ const result = await validate(req.body, {
134
+ slug: [rules.required, isSlug]
135
+ })`}
136
+ </CodeBlock>
137
+
138
+ <CodeBlock title="Async custom rule">
139
+ {`// Async rule for complex validation
140
+ const isAvailableUsername = async (value, field) => {
141
+ const existing = await (await db.table('users'))
142
+ .where('username', value)
143
+ .first()
144
+
145
+ if (existing) {
146
+ return 'Username is already taken'
147
+ }
148
+ return null
149
+ }
150
+
151
+ const result = await validate(req.body, {
152
+ username: [rules.required, isAvailableUsername]
153
+ })`}
154
+ </CodeBlock>
155
+ </Card>
156
+
157
+ <Card>
158
+ <h2 className="text-lg font-semibold mb-2">Optional Fields</h2>
159
+ <p className={`text-sm ${t.muted} mb-4`}>
160
+ Fields without the <code>required</code> rule are optional. Other rules only run if the field has a value.
161
+ </p>
162
+
163
+ <CodeBlock title="Optional field validation">
164
+ {`const result = await validate(req.body, {
165
+ name: [rules.required], // Required
166
+ bio: [rules.maxLength(500)], // Optional, but if provided must be <= 500 chars
167
+ website: [rules.url] // Optional, but if provided must be a valid URL
168
+ })`}
169
+ </CodeBlock>
170
+ </Card>
171
+
172
+ <Card>
173
+ <h2 className="text-lg font-semibold mb-2">Nested Objects</h2>
174
+ <p className={`text-sm ${t.muted} mb-4`}>
175
+ Validate nested objects using dot notation.
176
+ </p>
177
+
178
+ <CodeBlock title="Nested validation">
179
+ {`// Request body: { user: { name: 'John', email: 'john@example.com' } }
180
+
181
+ const result = await validate(req.body, {
182
+ 'user.name': [rules.required, rules.minLength(2)],
183
+ 'user.email': [rules.required, rules.email]
184
+ })`}
185
+ </CodeBlock>
186
+ </Card>
187
+
188
+ <Card>
189
+ <h2 className="text-lg font-semibold mb-2">Custom Error Messages</h2>
190
+ <p className={`text-sm ${t.muted} mb-4`}>
191
+ Override default error messages with custom ones.
192
+ </p>
193
+
194
+ <CodeBlock title="Custom messages">
195
+ {`const result = await validate(req.body, {
196
+ email: [
197
+ { rule: rules.required, message: 'Please enter your email' },
198
+ { rule: rules.email, message: 'Please enter a valid email address' }
199
+ ],
200
+ password: [
201
+ { rule: rules.required, message: 'Password is required' },
202
+ { rule: rules.minLength(8), message: 'Password must be at least 8 characters' }
203
+ ]
204
+ })`}
205
+ </CodeBlock>
206
+ </Card>
207
+ </div>
208
+ </div>
209
+ )
210
+ }
@@ -0,0 +1,81 @@
1
+ import { validate, rules } from '@basicbenframework/core/validation'
2
+ import { signJwt, verifyJwt, hashPassword, verifyPassword } from '@basicbenframework/core/auth'
3
+ import { User } from '../models/User.js'
4
+
5
+ export const AuthController = {
6
+ async register(req, res) {
7
+ const result = await validate(req.body, {
8
+ name: [rules.required, rules.string, rules.min(2)],
9
+ email: [rules.required, rules.email],
10
+ password: [rules.required, rules.min(8)]
11
+ })
12
+
13
+ if (result.fails()) {
14
+ return res.json({ errors: result.errors }, 422)
15
+ }
16
+
17
+ const { name, email, password } = req.body
18
+
19
+ // Check if email exists
20
+ const existing = await User.findByEmail(email)
21
+ if (existing) {
22
+ return res.json({ error: 'Email already registered' }, 400)
23
+ }
24
+
25
+ // Create user
26
+ const user = await User.create({
27
+ name,
28
+ email,
29
+ password: await hashPassword(password)
30
+ })
31
+
32
+ // Generate token
33
+ const token = signJwt({ userId: user.id }, process.env.APP_KEY, { expiresIn: '7d' })
34
+
35
+ res.json({
36
+ user: { id: user.id, name: user.name, email: user.email },
37
+ token
38
+ })
39
+ },
40
+
41
+ async login(req, res) {
42
+ const { email, password } = req.body
43
+
44
+ if (!email || !password) {
45
+ return res.json({ error: 'Email and password required' }, 400)
46
+ }
47
+
48
+ const user = await User.findByEmail(email)
49
+ if (!user || !(await verifyPassword(password, user.password))) {
50
+ return res.json({ error: 'Invalid credentials' }, 401)
51
+ }
52
+
53
+ const token = signJwt({ userId: user.id }, process.env.APP_KEY, { expiresIn: '7d' })
54
+
55
+ res.json({
56
+ user: { id: user.id, name: user.name, email: user.email },
57
+ token
58
+ })
59
+ },
60
+
61
+ async user(req, res) {
62
+ const token = req.headers.authorization?.replace('Bearer ', '')
63
+ if (!token) {
64
+ return res.json({ error: 'No token provided' }, 401)
65
+ }
66
+
67
+ const payload = verifyJwt(token, process.env.APP_KEY)
68
+ if (!payload) {
69
+ return res.json({ error: 'Invalid token' }, 401)
70
+ }
71
+
72
+ const user = await User.find(payload.userId)
73
+ if (!user) {
74
+ return res.json({ error: 'User not found' }, 404)
75
+ }
76
+
77
+ res.json({
78
+ user: { id: user.id, name: user.name, email: user.email }
79
+ })
80
+ }
81
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Home Controller
3
+ *
4
+ * Handles basic application endpoints.
5
+ */
6
+
7
+ export const HomeController = {
8
+ /**
9
+ * Hello endpoint
10
+ */
11
+ hello: async (req, res) => {
12
+ res.json({
13
+ message: 'Welcome to BasicBen!',
14
+ timestamp: new Date().toISOString()
15
+ })
16
+ }
17
+ }
@@ -0,0 +1,86 @@
1
+ import { validate, rules } from '@basicbenframework/core/validation'
2
+ import { Post } from '../models/Post.js'
3
+
4
+ export const PostController = {
5
+ async index(req, res) {
6
+ const posts = await Post.findByUser(req.userId)
7
+ res.json({ posts })
8
+ },
9
+
10
+ async show(req, res) {
11
+ const post = await Post.find(req.params.id)
12
+ if (!post || post.user_id !== req.userId) {
13
+ return res.json({ error: 'Post not found' }, 404)
14
+ }
15
+ res.json({ post })
16
+ },
17
+
18
+ async store(req, res) {
19
+ const result = await validate(req.body, {
20
+ title: [rules.required, rules.string, rules.min(3), rules.max(200)],
21
+ content: [rules.required, rules.string, rules.min(10)]
22
+ })
23
+
24
+ if (result.fails()) {
25
+ return res.json({ errors: result.errors }, 422)
26
+ }
27
+
28
+ const { title, content, published } = req.body
29
+ const post = await Post.create({
30
+ user_id: req.userId,
31
+ title,
32
+ content,
33
+ published: published || false
34
+ })
35
+
36
+ res.json({ post }, 201)
37
+ },
38
+
39
+ async update(req, res) {
40
+ const post = await Post.find(req.params.id)
41
+ if (!post || post.user_id !== req.userId) {
42
+ return res.json({ error: 'Post not found' }, 404)
43
+ }
44
+
45
+ const result = await validate(req.body, {
46
+ title: [rules.required, rules.string, rules.min(3), rules.max(200)],
47
+ content: [rules.required, rules.string, rules.min(10)]
48
+ })
49
+
50
+ if (result.fails()) {
51
+ return res.json({ errors: result.errors }, 422)
52
+ }
53
+
54
+ const { title, content, published } = req.body
55
+ const updated = await Post.update(req.params.id, {
56
+ title,
57
+ content,
58
+ published: published ? 1 : 0
59
+ })
60
+
61
+ res.json({ post: updated })
62
+ },
63
+
64
+ async destroy(req, res) {
65
+ const post = await Post.find(req.params.id)
66
+ if (!post || post.user_id !== req.userId) {
67
+ return res.json({ error: 'Post not found' }, 404)
68
+ }
69
+
70
+ await Post.delete(req.params.id)
71
+ res.json({ message: 'Post deleted' })
72
+ },
73
+
74
+ async feed(req, res) {
75
+ const posts = await Post.findPublished()
76
+ res.json({ posts })
77
+ },
78
+
79
+ async feedShow(req, res) {
80
+ const post = await Post.findPublishedById(req.params.id)
81
+ if (!post) {
82
+ return res.json({ error: 'Post not found' }, 404)
83
+ }
84
+ res.json({ post })
85
+ }
86
+ }