@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,156 @@
1
+ /**
2
+ * Database adapter loader.
3
+ * Provides a unified interface for SQLite, Postgres, Turso, PlanetScale, and Neon.
4
+ */
5
+
6
+ import { loadConfig } from '../server/loader.js'
7
+ import { QueryBuilder } from './QueryBuilder.js'
8
+
9
+ let dbInstance = null
10
+
11
+ /**
12
+ * Database adapter interface:
13
+ *
14
+ * - run(sql, params) → { lastInsertRowid, changes }
15
+ * - get(sql, params) → single row or undefined
16
+ * - all(sql, params) → array of rows
17
+ * - exec(sql) → run raw SQL (for migrations)
18
+ * - transaction(fn) → wrap fn in BEGIN/COMMIT
19
+ * - close() → close connection
20
+ */
21
+
22
+ /**
23
+ * Get or create database connection
24
+ */
25
+ export async function getDb() {
26
+ if (dbInstance) {
27
+ return dbInstance
28
+ }
29
+
30
+ const config = await loadConfig()
31
+ const dbConfig = config.db || {}
32
+
33
+ const driver = dbConfig.driver || 'sqlite'
34
+ const url = dbConfig.url || process.env.DATABASE_URL || './database.sqlite'
35
+
36
+ switch (driver) {
37
+ case 'sqlite':
38
+ case 'better-sqlite3': {
39
+ const { createSqliteAdapter } = await import('./adapters/sqlite.js')
40
+ dbInstance = await createSqliteAdapter(url, dbConfig)
41
+ break
42
+ }
43
+
44
+ case 'postgres':
45
+ case 'pg': {
46
+ const { createPostgresAdapter } = await import('./adapters/postgres.js')
47
+ dbInstance = await createPostgresAdapter(url, dbConfig)
48
+ break
49
+ }
50
+
51
+ case 'turso':
52
+ case 'libsql': {
53
+ const { createTursoAdapter } = await import('./adapters/turso.js')
54
+ dbInstance = await createTursoAdapter(url, dbConfig)
55
+ break
56
+ }
57
+
58
+ case 'planetscale':
59
+ case 'mysql': {
60
+ const { createPlanetScaleAdapter } = await import('./adapters/planetscale.js')
61
+ dbInstance = await createPlanetScaleAdapter(url, dbConfig)
62
+ break
63
+ }
64
+
65
+ case 'neon': {
66
+ const { createNeonAdapter } = await import('./adapters/neon.js')
67
+ dbInstance = await createNeonAdapter(url, dbConfig)
68
+ break
69
+ }
70
+
71
+ default:
72
+ throw new Error(
73
+ `Unknown database driver: ${driver}\n` +
74
+ 'Supported drivers: sqlite, postgres, turso, planetscale, neon'
75
+ )
76
+ }
77
+
78
+ return dbInstance
79
+ }
80
+
81
+ /**
82
+ * Shorthand exports for common operations
83
+ * These use the default connection
84
+ */
85
+ export const db = {
86
+ async run(sql, params) {
87
+ const conn = await getDb()
88
+ return conn.run(sql, params)
89
+ },
90
+
91
+ async get(sql, params) {
92
+ const conn = await getDb()
93
+ return conn.get(sql, params)
94
+ },
95
+
96
+ async all(sql, params) {
97
+ const conn = await getDb()
98
+ return conn.all(sql, params)
99
+ },
100
+
101
+ async exec(sql) {
102
+ const conn = await getDb()
103
+ return conn.exec(sql)
104
+ },
105
+
106
+ async transaction(fn) {
107
+ const conn = await getDb()
108
+ return conn.transaction(fn)
109
+ },
110
+
111
+ async close() {
112
+ if (dbInstance) {
113
+ await dbInstance.close()
114
+ dbInstance = null
115
+ }
116
+ },
117
+
118
+ /**
119
+ * Create a query builder for a table.
120
+ * Provides fluent API with mass assignment protection.
121
+ *
122
+ * @param {string} table - Table name
123
+ * @returns {Promise<QueryBuilder>}
124
+ */
125
+ async table(table) {
126
+ const conn = await getDb()
127
+ return new QueryBuilder(conn, table, conn.driver || 'sqlite')
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Reset connection (for testing)
133
+ */
134
+ export function resetDb() {
135
+ dbInstance = null
136
+ }
137
+
138
+ /**
139
+ * Create a query builder for a table.
140
+ * Provides fluent API with mass assignment protection.
141
+ *
142
+ * @param {string} table - Table name
143
+ * @returns {Promise<QueryBuilder>}
144
+ *
145
+ * @example
146
+ * const users = await query('users').where('active', true).get()
147
+ * await query('users').only('name', 'email').insert(req.body)
148
+ */
149
+ export async function query(table) {
150
+ const conn = await getDb()
151
+ return new QueryBuilder(conn, table, conn.driver || 'sqlite')
152
+ }
153
+
154
+ // Re-export QueryBuilder and Grammar for advanced usage
155
+ export { QueryBuilder } from './QueryBuilder.js'
156
+ export { Grammar } from './Grammar.js'
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Migration runner.
3
+ * Tracks migrations in _migrations table, runs in order.
4
+ */
5
+
6
+ import { readdirSync, existsSync } from 'node:fs'
7
+ import { join, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { getDb } from './index.js'
10
+
11
+ const MIGRATIONS_TABLE = '_migrations'
12
+
13
+ /**
14
+ * Create migrator instance
15
+ */
16
+ export async function createMigrator(migrationsDir = 'migrations') {
17
+ const db = await getDb()
18
+ const dir = resolve(process.cwd(), migrationsDir)
19
+
20
+ // Ensure migrations table exists
21
+ await ensureMigrationsTable(db)
22
+
23
+ return {
24
+ /**
25
+ * Run all pending migrations
26
+ */
27
+ async migrate() {
28
+ const pending = await getPendingMigrations(db, dir)
29
+
30
+ if (pending.length === 0) {
31
+ return { ran: [], message: 'Nothing to migrate.' }
32
+ }
33
+
34
+ const batch = await getNextBatch(db)
35
+ const ran = []
36
+
37
+ for (const migration of pending) {
38
+ const module = await loadMigration(migration.path)
39
+
40
+ try {
41
+ await module.up(db)
42
+ await recordMigration(db, migration.name, batch)
43
+ ran.push(migration.name)
44
+ } catch (err) {
45
+ throw new Error(`Migration failed: ${migration.name}\n${err.message}`)
46
+ }
47
+ }
48
+
49
+ return { ran, batch }
50
+ },
51
+
52
+ /**
53
+ * Roll back the last batch of migrations
54
+ */
55
+ async rollback() {
56
+ const lastBatch = await getLastBatch(db)
57
+
58
+ if (!lastBatch) {
59
+ return { rolledBack: [], message: 'Nothing to rollback.' }
60
+ }
61
+
62
+ const migrations = await getMigrationsByBatch(db, lastBatch)
63
+ const rolledBack = []
64
+
65
+ // Roll back in reverse order
66
+ for (const migration of migrations.reverse()) {
67
+ const filePath = findMigrationFile(dir, migration.migration)
68
+
69
+ if (!filePath) {
70
+ throw new Error(`Migration file not found: ${migration.migration}`)
71
+ }
72
+
73
+ const module = await loadMigration(filePath)
74
+
75
+ try {
76
+ await module.down(db)
77
+ await removeMigration(db, migration.migration)
78
+ rolledBack.push(migration.migration)
79
+ } catch (err) {
80
+ throw new Error(`Rollback failed: ${migration.migration}\n${err.message}`)
81
+ }
82
+ }
83
+
84
+ return { rolledBack, batch: lastBatch }
85
+ },
86
+
87
+ /**
88
+ * Drop all tables and re-run all migrations
89
+ */
90
+ async fresh() {
91
+ // Get all tables
92
+ const tables = await getAllTables(db)
93
+
94
+ // Drop all tables (except sqlite internal tables)
95
+ for (const table of tables) {
96
+ if (!table.startsWith('sqlite_')) {
97
+ await db.exec(`DROP TABLE IF EXISTS "${table}"`)
98
+ }
99
+ }
100
+
101
+ // Re-create migrations table
102
+ await ensureMigrationsTable(db)
103
+
104
+ // Run all migrations
105
+ return this.migrate()
106
+ },
107
+
108
+ /**
109
+ * Get migration status
110
+ */
111
+ async status() {
112
+ const files = getMigrationFiles(dir)
113
+ const ran = await getRanMigrations(db)
114
+ const ranSet = new Set(ran.map(m => m.migration))
115
+
116
+ return files.map(file => ({
117
+ name: file.name,
118
+ ran: ranSet.has(file.name),
119
+ batch: ran.find(m => m.migration === file.name)?.batch || null
120
+ }))
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Create migrations table if it doesn't exist
127
+ */
128
+ async function ensureMigrationsTable(db) {
129
+ await db.exec(`
130
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
131
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
132
+ migration TEXT NOT NULL UNIQUE,
133
+ batch INTEGER NOT NULL,
134
+ ran_at DATETIME DEFAULT CURRENT_TIMESTAMP
135
+ )
136
+ `)
137
+ }
138
+
139
+ /**
140
+ * Get list of migration files
141
+ */
142
+ function getMigrationFiles(dir) {
143
+ if (!existsSync(dir)) {
144
+ return []
145
+ }
146
+
147
+ return readdirSync(dir)
148
+ .filter(f => f.endsWith('.js') && !f.endsWith('.test.js'))
149
+ .sort()
150
+ .map(f => ({
151
+ name: f.replace('.js', ''),
152
+ path: join(dir, f)
153
+ }))
154
+ }
155
+
156
+ /**
157
+ * Get migrations that haven't been run
158
+ */
159
+ async function getPendingMigrations(db, dir) {
160
+ const files = getMigrationFiles(dir)
161
+ const ran = await getRanMigrations(db)
162
+ const ranSet = new Set(ran.map(m => m.migration))
163
+
164
+ return files.filter(f => !ranSet.has(f.name))
165
+ }
166
+
167
+ /**
168
+ * Get all ran migrations
169
+ */
170
+ async function getRanMigrations(db) {
171
+ return db.all(`SELECT * FROM ${MIGRATIONS_TABLE} ORDER BY batch, id`)
172
+ }
173
+
174
+ /**
175
+ * Get next batch number
176
+ */
177
+ async function getNextBatch(db) {
178
+ const result = await db.get(`SELECT MAX(batch) as max FROM ${MIGRATIONS_TABLE}`)
179
+ return (result?.max || 0) + 1
180
+ }
181
+
182
+ /**
183
+ * Get last batch number
184
+ */
185
+ async function getLastBatch(db) {
186
+ const result = await db.get(`SELECT MAX(batch) as max FROM ${MIGRATIONS_TABLE}`)
187
+ return result?.max || null
188
+ }
189
+
190
+ /**
191
+ * Get migrations by batch
192
+ */
193
+ async function getMigrationsByBatch(db, batch) {
194
+ return db.all(`SELECT * FROM ${MIGRATIONS_TABLE} WHERE batch = ? ORDER BY id`, [batch])
195
+ }
196
+
197
+ /**
198
+ * Record that a migration has run
199
+ */
200
+ async function recordMigration(db, name, batch) {
201
+ await db.run(
202
+ `INSERT INTO ${MIGRATIONS_TABLE} (migration, batch) VALUES (?, ?)`,
203
+ [name, batch]
204
+ )
205
+ }
206
+
207
+ /**
208
+ * Remove migration record
209
+ */
210
+ async function removeMigration(db, name) {
211
+ await db.run(`DELETE FROM ${MIGRATIONS_TABLE} WHERE migration = ?`, [name])
212
+ }
213
+
214
+ /**
215
+ * Load migration module
216
+ */
217
+ async function loadMigration(filePath) {
218
+ const fileUrl = pathToFileURL(filePath).href
219
+ return import(fileUrl)
220
+ }
221
+
222
+ /**
223
+ * Find migration file by name
224
+ */
225
+ function findMigrationFile(dir, name) {
226
+ const files = getMigrationFiles(dir)
227
+ const match = files.find(f => f.name === name)
228
+ return match?.path || null
229
+ }
230
+
231
+ /**
232
+ * Get all table names (for fresh command)
233
+ */
234
+ async function getAllTables(db) {
235
+ // SQLite
236
+ const sqliteTables = await db.all(
237
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
238
+ ).catch(() => [])
239
+
240
+ if (sqliteTables.length > 0) {
241
+ return sqliteTables.map(t => t.name)
242
+ }
243
+
244
+ // Postgres
245
+ const pgTables = await db.all(
246
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
247
+ ).catch(() => [])
248
+
249
+ return pgTables.map(t => t.tablename)
250
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Database seeder.
3
+ * Runs seed files to populate the database with initial/test data.
4
+ */
5
+
6
+ import { readdirSync, existsSync } from 'node:fs'
7
+ import { join, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { getDb } from './index.js'
10
+
11
+ /**
12
+ * Create seeder instance
13
+ *
14
+ * @param {string} seedsDir - Path to seeds directory (default: 'seeds')
15
+ */
16
+ export async function createSeeder(seedsDir = 'seeds') {
17
+ const db = await getDb()
18
+ const dir = resolve(process.cwd(), seedsDir)
19
+
20
+ return {
21
+ /**
22
+ * Run all seed files
23
+ *
24
+ * @returns {Promise<{ ran: string[] }>}
25
+ */
26
+ async runAll() {
27
+ const files = getSeedFiles(dir)
28
+
29
+ if (files.length === 0) {
30
+ return { ran: [], message: 'No seed files found.' }
31
+ }
32
+
33
+ const ran = []
34
+
35
+ for (const seed of files) {
36
+ await this.runSeed(seed.name, seed.path)
37
+ ran.push(seed.name)
38
+ }
39
+
40
+ return { ran }
41
+ },
42
+
43
+ /**
44
+ * Run a specific seed file
45
+ *
46
+ * @param {string} name - Seed file name (without extension)
47
+ * @returns {Promise<void>}
48
+ */
49
+ async run(name) {
50
+ const files = getSeedFiles(dir)
51
+ const seed = files.find(f => f.name === name || f.name === `${name}.js`)
52
+
53
+ if (!seed) {
54
+ throw new Error(`Seed file not found: ${name}`)
55
+ }
56
+
57
+ await this.runSeed(seed.name, seed.path)
58
+ return { ran: [seed.name] }
59
+ },
60
+
61
+ /**
62
+ * Run a seed file by path
63
+ *
64
+ * @param {string} name - Seed name for logging
65
+ * @param {string} filePath - Full path to seed file
66
+ */
67
+ async runSeed(name, filePath) {
68
+ const module = await loadSeed(filePath)
69
+
70
+ if (typeof module.seed !== 'function' && typeof module.default !== 'function') {
71
+ throw new Error(
72
+ `Seed file ${name} must export a 'seed' function or default function`
73
+ )
74
+ }
75
+
76
+ const seedFn = module.seed || module.default
77
+
78
+ try {
79
+ await seedFn(db)
80
+ } catch (err) {
81
+ throw new Error(`Seed failed: ${name}\n${err.message}`)
82
+ }
83
+ },
84
+
85
+ /**
86
+ * List all available seed files
87
+ *
88
+ * @returns {string[]}
89
+ */
90
+ list() {
91
+ return getSeedFiles(dir).map(f => f.name)
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get list of seed files
98
+ *
99
+ * @param {string} dir - Seeds directory
100
+ * @returns {{ name: string, path: string }[]}
101
+ */
102
+ function getSeedFiles(dir) {
103
+ if (!existsSync(dir)) {
104
+ return []
105
+ }
106
+
107
+ return readdirSync(dir)
108
+ .filter(f => f.endsWith('.js') && !f.endsWith('.test.js'))
109
+ .sort()
110
+ .map(f => ({
111
+ name: f.replace('.js', ''),
112
+ path: join(dir, f)
113
+ }))
114
+ }
115
+
116
+ /**
117
+ * Load seed module
118
+ *
119
+ * @param {string} filePath - Path to seed file
120
+ */
121
+ async function loadSeed(filePath) {
122
+ const fileUrl = pathToFileURL(filePath).href
123
+ return import(fileUrl)
124
+ }
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * BasicBen framework public API
3
+ */
4
+
5
+ export const VERSION = '0.1.0'
6
+
7
+ // Database
8
+ export { db, query, getDb, QueryBuilder, Grammar } from './db/index.js'
9
+
10
+ // These will be implemented in later phases
11
+ // export { validate, rules } from './validation/index.js'
12
+ // export { signJwt, verifyJwt } from './auth/jwt.js'
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Scaffolding system.
3
+ * Reads stub files, replaces placeholders, writes to target directory.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
7
+ import { join, dirname, resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+ const stubsDir = resolve(__dirname, '../../stubs')
12
+
13
+ /**
14
+ * Generate a file from a stub template
15
+ *
16
+ * @param {string} stubName - Name of stub file (without .stub extension)
17
+ * @param {string} targetPath - Where to write the generated file
18
+ * @param {Object} replacements - Key-value pairs for placeholder replacement
19
+ */
20
+ export function generate(stubName, targetPath, replacements = {}) {
21
+ const stubPath = join(stubsDir, `${stubName}.stub`)
22
+
23
+ if (!existsSync(stubPath)) {
24
+ throw new Error(`Stub not found: ${stubName}.stub`)
25
+ }
26
+
27
+ // Check if target already exists
28
+ const fullTargetPath = resolve(process.cwd(), targetPath)
29
+ if (existsSync(fullTargetPath)) {
30
+ throw new Error(`File already exists: ${targetPath}`)
31
+ }
32
+
33
+ // Read stub
34
+ let content = readFileSync(stubPath, 'utf8')
35
+
36
+ // Replace placeholders: {{name}}, {{Name}}, {{table}}, etc.
37
+ for (const [key, value] of Object.entries(replacements)) {
38
+ const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g')
39
+ content = content.replace(pattern, value)
40
+ }
41
+
42
+ // Ensure directory exists
43
+ const targetDir = dirname(fullTargetPath)
44
+ if (!existsSync(targetDir)) {
45
+ mkdirSync(targetDir, { recursive: true })
46
+ }
47
+
48
+ // Write file
49
+ writeFileSync(fullTargetPath, content)
50
+
51
+ return fullTargetPath
52
+ }
53
+
54
+ /**
55
+ * Transform name to different cases
56
+ */
57
+ export function transformName(name) {
58
+ // Remove common suffixes if present
59
+ const baseName = name
60
+ .replace(/Controller$/i, '')
61
+ .replace(/Model$/i, '')
62
+ .replace(/Middleware$/i, '')
63
+
64
+ return {
65
+ // Original input
66
+ original: name,
67
+
68
+ // PascalCase: UserController
69
+ pascal: toPascalCase(baseName),
70
+
71
+ // camelCase: userController
72
+ camel: toCamelCase(baseName),
73
+
74
+ // snake_case: user_controller
75
+ snake: toSnakeCase(baseName),
76
+
77
+ // kebab-case: user-controller
78
+ kebab: toKebabCase(baseName),
79
+
80
+ // lowercase: user
81
+ lower: baseName.toLowerCase(),
82
+
83
+ // Plural forms (simple)
84
+ pluralLower: pluralize(baseName.toLowerCase()),
85
+ pluralSnake: pluralize(toSnakeCase(baseName))
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Convert to PascalCase
91
+ */
92
+ function toPascalCase(str) {
93
+ return str
94
+ .replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '')
95
+ .replace(/^(.)/, (c) => c.toUpperCase())
96
+ }
97
+
98
+ /**
99
+ * Convert to camelCase
100
+ */
101
+ function toCamelCase(str) {
102
+ const pascal = toPascalCase(str)
103
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1)
104
+ }
105
+
106
+ /**
107
+ * Convert to snake_case
108
+ */
109
+ function toSnakeCase(str) {
110
+ return str
111
+ .replace(/([A-Z])/g, '_$1')
112
+ .toLowerCase()
113
+ .replace(/^_/, '')
114
+ .replace(/[-\s]+/g, '_')
115
+ }
116
+
117
+ /**
118
+ * Convert to kebab-case
119
+ */
120
+ function toKebabCase(str) {
121
+ return toSnakeCase(str).replace(/_/g, '-')
122
+ }
123
+
124
+ /**
125
+ * Simple pluralization
126
+ */
127
+ function pluralize(str) {
128
+ if (str.endsWith('y')) {
129
+ return str.slice(0, -1) + 'ies'
130
+ }
131
+ if (str.endsWith('s') || str.endsWith('x') || str.endsWith('ch') || str.endsWith('sh')) {
132
+ return str + 'es'
133
+ }
134
+ return str + 's'
135
+ }
136
+
137
+ /**
138
+ * Generate timestamp for migration files
139
+ */
140
+ export function timestamp() {
141
+ const now = new Date()
142
+ const pad = (n) => String(n).padStart(2, '0')
143
+
144
+ return [
145
+ now.getFullYear(),
146
+ pad(now.getMonth() + 1),
147
+ pad(now.getDate()),
148
+ pad(now.getHours()),
149
+ pad(now.getMinutes()),
150
+ pad(now.getSeconds())
151
+ ].join('')
152
+ }