@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,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation system.
|
|
3
|
+
* Simple, composable validation with async support.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from '../db/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate data against rules
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} data - Data to validate
|
|
12
|
+
* @param {Object} schema - Validation schema { field: [rules] }
|
|
13
|
+
* @returns {ValidationResult}
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const result = await validate(req.body, {
|
|
17
|
+
* email: [rules.required, rules.email],
|
|
18
|
+
* password: [rules.required, rules.min(8)]
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* if (result.fails()) {
|
|
22
|
+
* return res.status(422).json({ errors: result.errors })
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export async function validate(data, schema) {
|
|
26
|
+
const errors = {}
|
|
27
|
+
|
|
28
|
+
for (const [field, fieldRules] of Object.entries(schema)) {
|
|
29
|
+
const value = data[field]
|
|
30
|
+
const fieldErrors = []
|
|
31
|
+
|
|
32
|
+
for (const rule of fieldRules) {
|
|
33
|
+
// Skip remaining rules if field is optional and empty
|
|
34
|
+
if (rule === rules.optional) {
|
|
35
|
+
if (isEmpty(value)) break
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const error = await rule(value, field, data)
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
fieldErrors.push(error)
|
|
43
|
+
// Stop on first error for this field
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (fieldErrors.length > 0) {
|
|
49
|
+
errors[field] = fieldErrors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return new ValidationResult(errors)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validation result class
|
|
58
|
+
*/
|
|
59
|
+
class ValidationResult {
|
|
60
|
+
constructor(errors) {
|
|
61
|
+
this.errors = errors
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if validation failed
|
|
66
|
+
*/
|
|
67
|
+
fails() {
|
|
68
|
+
return Object.keys(this.errors).length > 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if validation passed
|
|
73
|
+
*/
|
|
74
|
+
passes() {
|
|
75
|
+
return !this.fails()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get first error for a field
|
|
80
|
+
*/
|
|
81
|
+
first(field) {
|
|
82
|
+
return this.errors[field]?.[0] || null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all errors as flat array
|
|
87
|
+
*/
|
|
88
|
+
all() {
|
|
89
|
+
const flat = []
|
|
90
|
+
for (const [field, messages] of Object.entries(this.errors)) {
|
|
91
|
+
for (const message of messages) {
|
|
92
|
+
flat.push({ field, message })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return flat
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Built-in validation rules
|
|
101
|
+
*/
|
|
102
|
+
export const rules = {
|
|
103
|
+
/**
|
|
104
|
+
* Field is required
|
|
105
|
+
*/
|
|
106
|
+
required: (value, field) => {
|
|
107
|
+
if (isEmpty(value)) {
|
|
108
|
+
return `${field} is required`
|
|
109
|
+
}
|
|
110
|
+
return null
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Field is optional (stops validation chain if empty)
|
|
115
|
+
*/
|
|
116
|
+
optional: 'optional', // Marker, handled specially in validate()
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Must be a string
|
|
120
|
+
*/
|
|
121
|
+
string: (value, field) => {
|
|
122
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
123
|
+
return `${field} must be a string`
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Must be a number or numeric string
|
|
130
|
+
*/
|
|
131
|
+
numeric: (value, field) => {
|
|
132
|
+
if (value !== undefined && value !== null && isNaN(Number(value))) {
|
|
133
|
+
return `${field} must be a number`
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Must be an integer
|
|
140
|
+
*/
|
|
141
|
+
integer: (value, field) => {
|
|
142
|
+
if (value !== undefined && value !== null && !Number.isInteger(Number(value))) {
|
|
143
|
+
return `${field} must be an integer`
|
|
144
|
+
}
|
|
145
|
+
return null
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Must be a boolean
|
|
150
|
+
*/
|
|
151
|
+
boolean: (value, field) => {
|
|
152
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
153
|
+
return `${field} must be a boolean`
|
|
154
|
+
}
|
|
155
|
+
return null
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Must be an array
|
|
160
|
+
*/
|
|
161
|
+
array: (value, field) => {
|
|
162
|
+
if (value !== undefined && value !== null && !Array.isArray(value)) {
|
|
163
|
+
return `${field} must be an array`
|
|
164
|
+
}
|
|
165
|
+
return null
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Must be a valid email
|
|
170
|
+
*/
|
|
171
|
+
email: (value, field) => {
|
|
172
|
+
if (isEmpty(value)) return null
|
|
173
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
174
|
+
if (!emailRegex.test(value)) {
|
|
175
|
+
return `${field} must be a valid email`
|
|
176
|
+
}
|
|
177
|
+
return null
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Must be a valid URL
|
|
182
|
+
*/
|
|
183
|
+
url: (value, field) => {
|
|
184
|
+
if (isEmpty(value)) return null
|
|
185
|
+
try {
|
|
186
|
+
new URL(value)
|
|
187
|
+
return null
|
|
188
|
+
} catch {
|
|
189
|
+
return `${field} must be a valid URL`
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Minimum length (string) or value (number)
|
|
195
|
+
*/
|
|
196
|
+
min: (minValue) => (value, field) => {
|
|
197
|
+
if (isEmpty(value)) return null
|
|
198
|
+
if (typeof value === 'string' && value.length < minValue) {
|
|
199
|
+
return `${field} must be at least ${minValue} characters`
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === 'number' && value < minValue) {
|
|
202
|
+
return `${field} must be at least ${minValue}`
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Maximum length (string) or value (number)
|
|
209
|
+
*/
|
|
210
|
+
max: (maxValue) => (value, field) => {
|
|
211
|
+
if (isEmpty(value)) return null
|
|
212
|
+
if (typeof value === 'string' && value.length > maxValue) {
|
|
213
|
+
return `${field} must be at most ${maxValue} characters`
|
|
214
|
+
}
|
|
215
|
+
if (typeof value === 'number' && value > maxValue) {
|
|
216
|
+
return `${field} must be at most ${maxValue}`
|
|
217
|
+
}
|
|
218
|
+
return null
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Value must be between min and max (inclusive)
|
|
223
|
+
*/
|
|
224
|
+
between: (min, max) => (value, field) => {
|
|
225
|
+
if (isEmpty(value)) return null
|
|
226
|
+
const num = Number(value)
|
|
227
|
+
if (isNaN(num) || num < min || num > max) {
|
|
228
|
+
return `${field} must be between ${min} and ${max}`
|
|
229
|
+
}
|
|
230
|
+
return null
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Value must be in allowed list
|
|
235
|
+
*/
|
|
236
|
+
in: (...allowed) => (value, field) => {
|
|
237
|
+
if (isEmpty(value)) return null
|
|
238
|
+
if (!allowed.includes(value)) {
|
|
239
|
+
return `${field} must be one of: ${allowed.join(', ')}`
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Value must not be in disallowed list
|
|
246
|
+
*/
|
|
247
|
+
notIn: (...disallowed) => (value, field) => {
|
|
248
|
+
if (isEmpty(value)) return null
|
|
249
|
+
if (disallowed.includes(value)) {
|
|
250
|
+
return `${field} must not be one of: ${disallowed.join(', ')}`
|
|
251
|
+
}
|
|
252
|
+
return null
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Must match regex pattern
|
|
257
|
+
*/
|
|
258
|
+
regex: (pattern) => (value, field) => {
|
|
259
|
+
if (isEmpty(value)) return null
|
|
260
|
+
if (!pattern.test(value)) {
|
|
261
|
+
return `${field} format is invalid`
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Must match another field (e.g., password confirmation)
|
|
268
|
+
*/
|
|
269
|
+
confirmed: (confirmField) => (value, field, data) => {
|
|
270
|
+
const confirmFieldName = confirmField || `${field}_confirmation`
|
|
271
|
+
if (value !== data[confirmFieldName]) {
|
|
272
|
+
return `${field} confirmation does not match`
|
|
273
|
+
}
|
|
274
|
+
return null
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Must be different from another field
|
|
279
|
+
*/
|
|
280
|
+
different: (otherField) => (value, field, data) => {
|
|
281
|
+
if (value === data[otherField]) {
|
|
282
|
+
return `${field} must be different from ${otherField}`
|
|
283
|
+
}
|
|
284
|
+
return null
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* String must match exact length
|
|
289
|
+
*/
|
|
290
|
+
length: (len) => (value, field) => {
|
|
291
|
+
if (isEmpty(value)) return null
|
|
292
|
+
if (typeof value === 'string' && value.length !== len) {
|
|
293
|
+
return `${field} must be exactly ${len} characters`
|
|
294
|
+
}
|
|
295
|
+
return null
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Must be alphanumeric
|
|
300
|
+
*/
|
|
301
|
+
alphanumeric: (value, field) => {
|
|
302
|
+
if (isEmpty(value)) return null
|
|
303
|
+
if (!/^[a-zA-Z0-9]+$/.test(value)) {
|
|
304
|
+
return `${field} must contain only letters and numbers`
|
|
305
|
+
}
|
|
306
|
+
return null
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Must be alpha only
|
|
311
|
+
*/
|
|
312
|
+
alpha: (value, field) => {
|
|
313
|
+
if (isEmpty(value)) return null
|
|
314
|
+
if (!/^[a-zA-Z]+$/.test(value)) {
|
|
315
|
+
return `${field} must contain only letters`
|
|
316
|
+
}
|
|
317
|
+
return null
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Must be a valid date
|
|
322
|
+
*/
|
|
323
|
+
date: (value, field) => {
|
|
324
|
+
if (isEmpty(value)) return null
|
|
325
|
+
const date = new Date(value)
|
|
326
|
+
if (isNaN(date.getTime())) {
|
|
327
|
+
return `${field} must be a valid date`
|
|
328
|
+
}
|
|
329
|
+
return null
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Date must be before another date
|
|
334
|
+
*/
|
|
335
|
+
before: (dateStr) => (value, field) => {
|
|
336
|
+
if (isEmpty(value)) return null
|
|
337
|
+
const date = new Date(value)
|
|
338
|
+
const beforeDate = new Date(dateStr)
|
|
339
|
+
if (date >= beforeDate) {
|
|
340
|
+
return `${field} must be before ${dateStr}`
|
|
341
|
+
}
|
|
342
|
+
return null
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Date must be after another date
|
|
347
|
+
*/
|
|
348
|
+
after: (dateStr) => (value, field) => {
|
|
349
|
+
if (isEmpty(value)) return null
|
|
350
|
+
const date = new Date(value)
|
|
351
|
+
const afterDate = new Date(dateStr)
|
|
352
|
+
if (date <= afterDate) {
|
|
353
|
+
return `${field} must be after ${dateStr}`
|
|
354
|
+
}
|
|
355
|
+
return null
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Value must be unique in database table
|
|
360
|
+
*
|
|
361
|
+
* @param {string} table - Table name
|
|
362
|
+
* @param {string} [column] - Column name (defaults to field name)
|
|
363
|
+
* @param {number|string} [exceptId] - ID to exclude (for updates)
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* // Check email is unique
|
|
367
|
+
* email: [rules.unique('users')]
|
|
368
|
+
*
|
|
369
|
+
* // Check email is unique, excluding current user
|
|
370
|
+
* email: [rules.unique('users', 'email', userId)]
|
|
371
|
+
*/
|
|
372
|
+
unique: (table, column, exceptId) => async (value, field) => {
|
|
373
|
+
if (isEmpty(value)) return null
|
|
374
|
+
|
|
375
|
+
const col = column || field
|
|
376
|
+
const query = await db.table(table)
|
|
377
|
+
|
|
378
|
+
query.where(col, value)
|
|
379
|
+
|
|
380
|
+
if (exceptId !== undefined) {
|
|
381
|
+
query.where('id', '!=', exceptId)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const exists = await query.exists()
|
|
385
|
+
|
|
386
|
+
if (exists) {
|
|
387
|
+
return `${field} has already been taken`
|
|
388
|
+
}
|
|
389
|
+
return null
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Value must exist in database table
|
|
394
|
+
*
|
|
395
|
+
* @param {string} table - Table name
|
|
396
|
+
* @param {string} [column] - Column name (defaults to 'id')
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* // Check user_id exists in users table
|
|
400
|
+
* user_id: [rules.exists('users')]
|
|
401
|
+
*
|
|
402
|
+
* // Check category exists by slug
|
|
403
|
+
* category: [rules.exists('categories', 'slug')]
|
|
404
|
+
*/
|
|
405
|
+
exists: (table, column = 'id') => async (value, field) => {
|
|
406
|
+
if (isEmpty(value)) return null
|
|
407
|
+
|
|
408
|
+
const query = await db.table(table)
|
|
409
|
+
const exists = await query.where(column, value).exists()
|
|
410
|
+
|
|
411
|
+
if (!exists) {
|
|
412
|
+
return `${field} does not exist`
|
|
413
|
+
}
|
|
414
|
+
return null
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Check if value is empty
|
|
420
|
+
*/
|
|
421
|
+
function isEmpty(value) {
|
|
422
|
+
return value === undefined || value === null || value === ''
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create custom rule with custom message
|
|
427
|
+
*/
|
|
428
|
+
export function rule(validator, message) {
|
|
429
|
+
return async (value, field, data) => {
|
|
430
|
+
const result = await validator(value, field, data)
|
|
431
|
+
if (result === false) {
|
|
432
|
+
return typeof message === 'function' ? message(field, value) : message
|
|
433
|
+
}
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BasicBen Vite configuration.
|
|
3
|
+
* Provides base config with API proxy and React plugin.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import react from '@vitejs/plugin-react'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create Vite config with BasicBen defaults
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options - Override options
|
|
12
|
+
* @returns {Object} Vite config
|
|
13
|
+
*/
|
|
14
|
+
export function defineConfig(options = {}) {
|
|
15
|
+
const apiPort = process.env.VITE_API_PORT || 3001
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
plugins: [
|
|
19
|
+
react(),
|
|
20
|
+
...(options.plugins || [])
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
server: {
|
|
24
|
+
port: 3000,
|
|
25
|
+
strictPort: true,
|
|
26
|
+
proxy: {
|
|
27
|
+
'/api': {
|
|
28
|
+
target: `http://localhost:${apiPort}`,
|
|
29
|
+
changeOrigin: true
|
|
30
|
+
},
|
|
31
|
+
...options.proxy
|
|
32
|
+
},
|
|
33
|
+
...options.server
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
build: {
|
|
37
|
+
outDir: 'dist/client',
|
|
38
|
+
...options.build
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Merge any additional options
|
|
42
|
+
...options
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default export for direct use in vite.config.js
|
|
48
|
+
*/
|
|
49
|
+
export default defineConfig()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}}Controller
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const {{Name}}Controller = {
|
|
6
|
+
/**
|
|
7
|
+
* List all {{pluralLower}}
|
|
8
|
+
* GET /{{pluralLower}}
|
|
9
|
+
*/
|
|
10
|
+
index: async (req, res) => {
|
|
11
|
+
res.json({ message: '{{Name}} index' })
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Show a single {{lower}}
|
|
16
|
+
* GET /{{pluralLower}}/:id
|
|
17
|
+
*/
|
|
18
|
+
show: async (req, res) => {
|
|
19
|
+
const { id } = req.params
|
|
20
|
+
res.json({ message: `{{Name}} show ${id}` })
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new {{lower}}
|
|
25
|
+
* POST /{{pluralLower}}
|
|
26
|
+
*/
|
|
27
|
+
create: async (req, res) => {
|
|
28
|
+
res.status(201).json({ message: '{{Name}} created' })
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update a {{lower}}
|
|
33
|
+
* PUT /{{pluralLower}}/:id
|
|
34
|
+
*/
|
|
35
|
+
update: async (req, res) => {
|
|
36
|
+
const { id } = req.params
|
|
37
|
+
res.json({ message: `{{Name}} updated ${id}` })
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Delete a {{lower}}
|
|
42
|
+
* DELETE /{{pluralLower}}/:id
|
|
43
|
+
*/
|
|
44
|
+
destroy: async (req, res) => {
|
|
45
|
+
const { id } = req.params
|
|
46
|
+
res.status(204).send()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth middleware
|
|
3
|
+
*
|
|
4
|
+
* Verifies JWT token from Authorization header.
|
|
5
|
+
* Adds user payload to req.user if valid.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { verifyJwt } from 'basicben/auth'
|
|
9
|
+
|
|
10
|
+
export default async (req, res, next) => {
|
|
11
|
+
const authHeader = req.headers.authorization
|
|
12
|
+
|
|
13
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
14
|
+
res.statusCode = 401
|
|
15
|
+
return res.json({ error: 'No token provided' })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const token = authHeader.slice(7) // Remove 'Bearer '
|
|
19
|
+
|
|
20
|
+
const payload = verifyJwt(token, process.env.APP_KEY)
|
|
21
|
+
|
|
22
|
+
if (!payload) {
|
|
23
|
+
res.statusCode = 401
|
|
24
|
+
return res.json({ error: 'Invalid or expired token' })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
req.user = payload
|
|
28
|
+
next()
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: {{description}}
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const up = async (db) => {
|
|
6
|
+
await db.exec(`
|
|
7
|
+
CREATE TABLE {{tableName}} (
|
|
8
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
10
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
11
|
+
)
|
|
12
|
+
`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const down = async (db) => {
|
|
16
|
+
await db.exec('DROP TABLE IF EXISTS {{tableName}}')
|
|
17
|
+
}
|
package/stubs/model.stub
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} model
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { db } from 'basicben'
|
|
6
|
+
|
|
7
|
+
export const {{Name}} = {
|
|
8
|
+
/**
|
|
9
|
+
* Table name
|
|
10
|
+
*/
|
|
11
|
+
table: '{{tableName}}',
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Columns that can be mass-assigned.
|
|
15
|
+
* Add column names here to allow them in create/update operations.
|
|
16
|
+
*/
|
|
17
|
+
fillable: [
|
|
18
|
+
// 'name',
|
|
19
|
+
// 'email',
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all {{pluralLower}}
|
|
24
|
+
*/
|
|
25
|
+
all: async () => {
|
|
26
|
+
return (await db.table('{{tableName}}')).get()
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find {{lower}} by ID
|
|
31
|
+
*/
|
|
32
|
+
find: async (id) => {
|
|
33
|
+
return (await db.table('{{tableName}}')).find(id)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new {{lower}}
|
|
38
|
+
*
|
|
39
|
+
* Uses mass assignment protection - only columns in `fillable` are allowed.
|
|
40
|
+
*/
|
|
41
|
+
create: async (data) => {
|
|
42
|
+
return (await db.table('{{tableName}}'))
|
|
43
|
+
.only(...{{Name}}.fillable)
|
|
44
|
+
.insert(data)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Update a {{lower}}
|
|
49
|
+
*
|
|
50
|
+
* Uses mass assignment protection - only columns in `fillable` are allowed.
|
|
51
|
+
*/
|
|
52
|
+
update: async (id, data) => {
|
|
53
|
+
return (await db.table('{{tableName}}'))
|
|
54
|
+
.only(...{{Name}}.fillable)
|
|
55
|
+
.where('id', id)
|
|
56
|
+
.update(data)
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Delete a {{lower}}
|
|
61
|
+
*/
|
|
62
|
+
destroy: async (id) => {
|
|
63
|
+
return (await db.table('{{tableName}}'))
|
|
64
|
+
.where('id', id)
|
|
65
|
+
.delete()
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Query builder for custom queries.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const active = await {{Name}}.query().where('active', true).get()
|
|
73
|
+
*/
|
|
74
|
+
query: async () => {
|
|
75
|
+
return db.table('{{tableName}}')
|
|
76
|
+
}
|
|
77
|
+
}
|
package/stubs/route.stub
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { {{Name}}Controller } from '../controllers/{{Name}}Controller.js'
|
|
6
|
+
|
|
7
|
+
export default (router) => {
|
|
8
|
+
router.get('/{{pluralLower}}', {{Name}}Controller.index)
|
|
9
|
+
router.get('/{{pluralLower}}/:id', {{Name}}Controller.show)
|
|
10
|
+
router.post('/{{pluralLower}}', {{Name}}Controller.create)
|
|
11
|
+
router.put('/{{pluralLower}}/:id', {{Name}}Controller.update)
|
|
12
|
+
router.delete('/{{pluralLower}}/:id', {{Name}}Controller.destroy)
|
|
13
|
+
}
|
package/stubs/seed.stub
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{name}} seeder
|
|
3
|
+
* Run with: basicben seed {{lower}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db } from 'basicben'
|
|
7
|
+
|
|
8
|
+
export async function seed() {
|
|
9
|
+
// Example: Insert sample data
|
|
10
|
+
// await (await db.table('{{tableName}}')).insert({
|
|
11
|
+
// name: 'Sample',
|
|
12
|
+
// created_at: new Date().toISOString()
|
|
13
|
+
// })
|
|
14
|
+
|
|
15
|
+
console.log('{{name}} seeder completed')
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
port: 3000,
|
|
8
|
+
proxy: {
|
|
9
|
+
'/api': {
|
|
10
|
+
target: 'http://localhost:3001',
|
|
11
|
+
changeOrigin: true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
build: {
|
|
16
|
+
outDir: 'dist/client'
|
|
17
|
+
}
|
|
18
|
+
})
|