@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,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,9 @@
1
+ /**
2
+ * {{Name}} middleware
3
+ */
4
+
5
+ export default async (req, res, next) => {
6
+ // Add your middleware logic here
7
+
8
+ next()
9
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ })