@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
package/src/db/index.js
ADDED
|
@@ -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
|
+
}
|
package/src/db/seeder.js
ADDED
|
@@ -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
|
+
}
|