@basicbenframework/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +35 -0
- package/README.md +588 -0
- package/bin/cli.js +8 -0
- package/create-basicben-app/index.js +205 -0
- package/create-basicben-app/package.json +30 -0
- package/create-basicben-app/template/.env.example +24 -0
- package/create-basicben-app/template/README.md +59 -0
- package/create-basicben-app/template/basicben.config.js +33 -0
- package/create-basicben-app/template/index.html +54 -0
- package/create-basicben-app/template/migrations/001_create_users.js +15 -0
- package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
- package/create-basicben-app/template/public/.gitkeep +0 -0
- package/create-basicben-app/template/seeds/01_users.js +29 -0
- package/create-basicben-app/template/seeds/02_posts.js +43 -0
- package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
- package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
- package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
- package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
- package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
- package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
- package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
- package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
- package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
- package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
- package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
- package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
- package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
- package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
- package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
- package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
- package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
- package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
- package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
- package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
- package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
- package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
- package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
- package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
- package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
- package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
- package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
- package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
- package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
- package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
- package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
- package/create-basicben-app/template/src/controllers/PostController.js +86 -0
- package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
- package/create-basicben-app/template/src/helpers/api.js +24 -0
- package/create-basicben-app/template/src/main.jsx +9 -0
- package/create-basicben-app/template/src/middleware/auth.js +16 -0
- package/create-basicben-app/template/src/models/Post.js +63 -0
- package/create-basicben-app/template/src/models/User.js +42 -0
- package/create-basicben-app/template/src/routes/App.jsx +38 -0
- package/create-basicben-app/template/src/routes/api/auth.js +7 -0
- package/create-basicben-app/template/src/routes/api/posts.js +15 -0
- package/create-basicben-app/template/src/routes/api/profile.js +8 -0
- package/create-basicben-app/template/src/server/index.js +16 -0
- package/create-basicben-app/template/vite.config.js +18 -0
- package/database.sqlite +0 -0
- package/my-test-app/.env.example +24 -0
- package/my-test-app/README.md +59 -0
- package/my-test-app/basicben.config.js +33 -0
- package/my-test-app/database.sqlite-shm +0 -0
- package/my-test-app/database.sqlite-wal +0 -0
- package/my-test-app/index.html +54 -0
- package/my-test-app/migrations/001_create_users.js +15 -0
- package/my-test-app/migrations/002_create_posts.js +18 -0
- package/my-test-app/package-lock.json +2160 -0
- package/my-test-app/package.json +29 -0
- package/my-test-app/public/.gitkeep +0 -0
- package/my-test-app/seeds/01_users.js +29 -0
- package/my-test-app/seeds/02_posts.js +43 -0
- package/my-test-app/src/client/components/Alert.jsx +11 -0
- package/my-test-app/src/client/components/Avatar.jsx +11 -0
- package/my-test-app/src/client/components/BackLink.jsx +10 -0
- package/my-test-app/src/client/components/Button.jsx +19 -0
- package/my-test-app/src/client/components/Card.jsx +10 -0
- package/my-test-app/src/client/components/Empty.jsx +6 -0
- package/my-test-app/src/client/components/Input.jsx +12 -0
- package/my-test-app/src/client/components/Loading.jsx +6 -0
- package/my-test-app/src/client/components/Logo.jsx +40 -0
- package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
- package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
- package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
- package/my-test-app/src/client/components/NavLink.jsx +10 -0
- package/my-test-app/src/client/components/PageHeader.jsx +8 -0
- package/my-test-app/src/client/components/PostCard.jsx +19 -0
- package/my-test-app/src/client/components/Textarea.jsx +12 -0
- package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
- package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
- package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
- package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
- package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
- package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
- package/my-test-app/src/client/pages/Auth.jsx +55 -0
- package/my-test-app/src/client/pages/Authentication.jsx +236 -0
- package/my-test-app/src/client/pages/Database.jsx +426 -0
- package/my-test-app/src/client/pages/Feed.jsx +34 -0
- package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
- package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
- package/my-test-app/src/client/pages/Home.jsx +206 -0
- package/my-test-app/src/client/pages/PostForm.jsx +69 -0
- package/my-test-app/src/client/pages/Posts.jsx +59 -0
- package/my-test-app/src/client/pages/Profile.jsx +68 -0
- package/my-test-app/src/client/pages/Routing.jsx +207 -0
- package/my-test-app/src/client/pages/Testing.jsx +251 -0
- package/my-test-app/src/client/pages/Validation.jsx +210 -0
- package/my-test-app/src/controllers/AuthController.js +81 -0
- package/my-test-app/src/controllers/HomeController.js +17 -0
- package/my-test-app/src/controllers/PostController.js +86 -0
- package/my-test-app/src/controllers/ProfileController.js +66 -0
- package/my-test-app/src/helpers/api.js +24 -0
- package/my-test-app/src/main.jsx +9 -0
- package/my-test-app/src/middleware/auth.js +16 -0
- package/my-test-app/src/models/Post.js +63 -0
- package/my-test-app/src/models/User.js +42 -0
- package/my-test-app/src/routes/App.jsx +38 -0
- package/my-test-app/src/routes/api/auth.js +7 -0
- package/my-test-app/src/routes/api/posts.js +15 -0
- package/my-test-app/src/routes/api/profile.js +8 -0
- package/my-test-app/src/server/index.js +16 -0
- package/my-test-app/vite.config.js +18 -0
- package/package.json +61 -0
- package/scripts/test-app.sh +59 -0
- package/src/auth/jwt.js +195 -0
- package/src/auth/password.js +132 -0
- package/src/cli/colors.js +31 -0
- package/src/cli/dispatcher.js +168 -0
- package/src/cli/parser.js +91 -0
- package/src/client/context.js +4 -0
- package/src/client/hooks.js +50 -0
- package/src/client/index.js +3 -0
- package/src/client/router.js +184 -0
- package/src/commands/build.js +155 -0
- package/src/commands/dev.js +206 -0
- package/src/commands/help.js +84 -0
- package/src/commands/make-controller.js +36 -0
- package/src/commands/make-middleware.js +44 -0
- package/src/commands/make-migration.js +51 -0
- package/src/commands/make-model.js +38 -0
- package/src/commands/make-route.js +36 -0
- package/src/commands/make-seed.js +38 -0
- package/src/commands/migrate-fresh.js +32 -0
- package/src/commands/migrate-rollback.js +30 -0
- package/src/commands/migrate-status.js +41 -0
- package/src/commands/migrate.js +30 -0
- package/src/commands/seed.js +47 -0
- package/src/commands/start.js +69 -0
- package/src/commands/test.js +46 -0
- package/src/db/Grammar.js +125 -0
- package/src/db/QueryBuilder.js +476 -0
- package/src/db/adapters/neon.js +170 -0
- package/src/db/adapters/planetscale.js +146 -0
- package/src/db/adapters/postgres.js +166 -0
- package/src/db/adapters/sqlite.js +125 -0
- package/src/db/adapters/turso.js +165 -0
- package/src/db/index.js +156 -0
- package/src/db/migrator.js +250 -0
- package/src/db/seeder.js +124 -0
- package/src/index.js +12 -0
- package/src/scaffolding/index.js +152 -0
- package/src/server/body-parser.js +159 -0
- package/src/server/cors.js +63 -0
- package/src/server/default-entry.js +13 -0
- package/src/server/http.js +221 -0
- package/src/server/index.js +168 -0
- package/src/server/loader.js +128 -0
- package/src/server/router.js +281 -0
- package/src/server/static.js +139 -0
- package/src/validation/index.js +436 -0
- package/src/vite/config.js +49 -0
- package/stubs/controller.stub +48 -0
- package/stubs/middleware-auth.stub +29 -0
- package/stubs/middleware.stub +9 -0
- package/stubs/migration.stub +17 -0
- package/stubs/model.stub +77 -0
- package/stubs/route.stub +13 -0
- package/stubs/seed.stub +16 -0
- package/stubs/vite.config.stub +18 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate:fresh command
|
|
3
|
+
* Drops all tables and re-runs all migrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createMigrator } from '../db/migrator.js'
|
|
7
|
+
import { green, yellow, cyan, dim, red } from '../cli/colors.js'
|
|
8
|
+
|
|
9
|
+
export async function run(args, flags) {
|
|
10
|
+
console.log(`\n${yellow('Dropping all tables...')}\n`)
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const migrator = await createMigrator()
|
|
14
|
+
const result = await migrator.fresh()
|
|
15
|
+
|
|
16
|
+
console.log(`${cyan('Running migrations...')}\n`)
|
|
17
|
+
|
|
18
|
+
if (result.ran.length === 0) {
|
|
19
|
+
console.log(`${yellow('No migrations to run.')}\n`)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const name of result.ran) {
|
|
24
|
+
console.log(`${green('✓')} ${name}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`\n${green('Fresh migration complete:')} ${result.ran.length} file(s)\n`)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(`\n${red('Fresh migration failed:')}\n${err.message}\n`)
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate:rollback command
|
|
3
|
+
* Rolls back the last batch of migrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createMigrator } from '../db/migrator.js'
|
|
7
|
+
import { green, yellow, cyan, dim, red } from '../cli/colors.js'
|
|
8
|
+
|
|
9
|
+
export async function run(args, flags) {
|
|
10
|
+
console.log(`\n${cyan('Rolling back migrations...')}\n`)
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const migrator = await createMigrator()
|
|
14
|
+
const result = await migrator.rollback()
|
|
15
|
+
|
|
16
|
+
if (result.rolledBack.length === 0) {
|
|
17
|
+
console.log(`${yellow('Nothing to rollback.')}\n`)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const name of result.rolledBack) {
|
|
22
|
+
console.log(`${green('✓')} ${name}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`\n${green('Rolled back:')} ${result.rolledBack.length} file(s) ${dim(`(batch ${result.batch})`)}\n`)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`\n${red('Rollback failed:')}\n${err.message}\n`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate:status command
|
|
3
|
+
* Shows which migrations have run
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createMigrator } from '../db/migrator.js'
|
|
7
|
+
import { green, yellow, cyan, dim, red } from '../cli/colors.js'
|
|
8
|
+
|
|
9
|
+
export async function run(args, flags) {
|
|
10
|
+
try {
|
|
11
|
+
const migrator = await createMigrator()
|
|
12
|
+
const status = await migrator.status()
|
|
13
|
+
|
|
14
|
+
if (status.length === 0) {
|
|
15
|
+
console.log(`\n${yellow('No migrations found.')}\n`)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`\n${cyan('Migration status:')}\n`)
|
|
20
|
+
|
|
21
|
+
const maxNameLen = Math.max(...status.map(s => s.name.length))
|
|
22
|
+
|
|
23
|
+
for (const migration of status) {
|
|
24
|
+
const name = migration.name.padEnd(maxNameLen)
|
|
25
|
+
const statusIcon = migration.ran ? green('✓ Ran') : yellow('○ Pending')
|
|
26
|
+
const batch = migration.batch ? dim(` (batch ${migration.batch})`) : ''
|
|
27
|
+
|
|
28
|
+
console.log(` ${statusIcon} ${name}${batch}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ranCount = status.filter(s => s.ran).length
|
|
32
|
+
const pendingCount = status.filter(s => !s.ran).length
|
|
33
|
+
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(` ${green('Ran:')} ${ranCount} ${yellow('Pending:')} ${pendingCount}`)
|
|
36
|
+
console.log()
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(`\n${red('Error:')}\n${err.message}\n`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate command
|
|
3
|
+
* Runs all pending migrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createMigrator } from '../db/migrator.js'
|
|
7
|
+
import { green, yellow, cyan, dim, red } from '../cli/colors.js'
|
|
8
|
+
|
|
9
|
+
export async function run(args, flags) {
|
|
10
|
+
console.log(`\n${cyan('Running migrations...')}\n`)
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const migrator = await createMigrator()
|
|
14
|
+
const result = await migrator.migrate()
|
|
15
|
+
|
|
16
|
+
if (result.ran.length === 0) {
|
|
17
|
+
console.log(`${yellow('Nothing to migrate.')}\n`)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const name of result.ran) {
|
|
22
|
+
console.log(`${green('✓')} ${name}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`\n${green('Migrated:')} ${result.ran.length} file(s) ${dim(`(batch ${result.batch})`)}\n`)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`\n${red('Migration failed:')}\n${err.message}\n`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seed command
|
|
3
|
+
* Runs database seeders to populate data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createSeeder } from '../db/seeder.js'
|
|
7
|
+
import { green, yellow, cyan, dim, red } from '../cli/colors.js'
|
|
8
|
+
|
|
9
|
+
export async function run(args, flags) {
|
|
10
|
+
const seedName = args[0]
|
|
11
|
+
|
|
12
|
+
if (seedName) {
|
|
13
|
+
console.log(`\n${cyan(`Running seed: ${seedName}...`)}\n`)
|
|
14
|
+
} else {
|
|
15
|
+
console.log(`\n${cyan('Running all seeds...')}\n`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const seeder = await createSeeder()
|
|
20
|
+
|
|
21
|
+
// Run specific seed or all seeds
|
|
22
|
+
const result = seedName
|
|
23
|
+
? await seeder.run(seedName)
|
|
24
|
+
: await seeder.runAll()
|
|
25
|
+
|
|
26
|
+
if (result.ran.length === 0) {
|
|
27
|
+
console.log(`${yellow('No seeds to run.')}\n`)
|
|
28
|
+
|
|
29
|
+
// Show hint if no seeds directory
|
|
30
|
+
const available = seeder.list()
|
|
31
|
+
if (available.length === 0) {
|
|
32
|
+
console.log(`${dim('Create seed files in seeds/ directory.')}`)
|
|
33
|
+
console.log(`${dim('Run: basicben make:seed <name>')}\n`)
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const name of result.ran) {
|
|
39
|
+
console.log(`${green('✓')} ${name}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`\n${green('Seeded:')} ${result.ran.length} file(s)\n`)
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`\n${red('Seed failed:')}\n${err.message}\n`)
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* start command
|
|
3
|
+
* Runs the production server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import { resolve } from 'node:path'
|
|
9
|
+
import { bold, cyan, green, yellow, dim, red } from '../cli/colors.js'
|
|
10
|
+
|
|
11
|
+
export async function run(args, flags) {
|
|
12
|
+
const cwd = process.cwd()
|
|
13
|
+
|
|
14
|
+
console.log(`\n${bold('BasicBen')} ${dim('start')}\n`)
|
|
15
|
+
|
|
16
|
+
// Check for built files
|
|
17
|
+
const distServer = resolve(cwd, 'dist/server/index.js')
|
|
18
|
+
const distClient = resolve(cwd, 'dist/client')
|
|
19
|
+
|
|
20
|
+
if (!existsSync(distServer)) {
|
|
21
|
+
console.error(`${red('Error:')} Production build not found.`)
|
|
22
|
+
console.error(`\nRun ${cyan('basicben build')} first.\n`)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!existsSync(distClient)) {
|
|
27
|
+
console.error(`${yellow('Warning:')} Client build not found at dist/client`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const port = flags.port || process.env.PORT || 3000
|
|
31
|
+
|
|
32
|
+
console.log(`${green('Starting production server...')}\n`)
|
|
33
|
+
console.log(`${cyan('Server')} ${dim('→')} http://localhost:${port}\n`)
|
|
34
|
+
|
|
35
|
+
// Start the production server
|
|
36
|
+
const nodeArgs = ['dist/server/index.js']
|
|
37
|
+
|
|
38
|
+
// Add .env file if it exists
|
|
39
|
+
if (existsSync(resolve(cwd, '.env'))) {
|
|
40
|
+
nodeArgs.unshift('--env-file=.env')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const proc = spawn('node', nodeArgs, {
|
|
44
|
+
cwd,
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
env: {
|
|
47
|
+
...process.env,
|
|
48
|
+
PORT: port,
|
|
49
|
+
NODE_ENV: 'production'
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
proc.on('exit', (code) => {
|
|
54
|
+
if (code !== 0) {
|
|
55
|
+
console.error(`\n${red('Server exited with code')} ${code}\n`)
|
|
56
|
+
process.exit(code)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Handle graceful shutdown
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
console.log(`\n${dim('Shutting down...')}\n`)
|
|
63
|
+
proc.kill()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
process.on('SIGINT', cleanup)
|
|
68
|
+
process.on('SIGTERM', cleanup)
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test command
|
|
3
|
+
* Runs Vitest for user app tests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { bold, cyan, dim } from '../cli/colors.js'
|
|
8
|
+
|
|
9
|
+
export async function run(args, flags) {
|
|
10
|
+
const cwd = process.cwd()
|
|
11
|
+
|
|
12
|
+
console.log(`\n${bold('BasicBen')} ${dim('test')}\n`)
|
|
13
|
+
|
|
14
|
+
// Build vitest args
|
|
15
|
+
const vitestArgs = ['vitest', ...args]
|
|
16
|
+
|
|
17
|
+
// Add common flags
|
|
18
|
+
if (flags.watch || flags.w) {
|
|
19
|
+
// Default is watch mode, no flag needed
|
|
20
|
+
} else if (!args.includes('--watch')) {
|
|
21
|
+
vitestArgs.push('--run') // Run once and exit
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (flags.coverage) {
|
|
25
|
+
vitestArgs.push('--coverage')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (flags.ui) {
|
|
29
|
+
vitestArgs.push('--ui')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`${cyan('Running tests with Vitest...')}\n`)
|
|
33
|
+
|
|
34
|
+
const proc = spawn('npx', vitestArgs, {
|
|
35
|
+
cwd,
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
NODE_ENV: 'test'
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
proc.on('exit', (code) => {
|
|
44
|
+
process.exit(code || 0)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Grammar - handles dialect differences and identifier escaping.
|
|
3
|
+
* Provides protection against SQL injection for identifiers (column/table names).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class Grammar {
|
|
7
|
+
constructor(driver = 'sqlite') {
|
|
8
|
+
this.driver = driver
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate an identifier (column/table name).
|
|
13
|
+
* Only allows alphanumeric characters and underscores.
|
|
14
|
+
* Must start with a letter or underscore.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} name - The identifier to validate
|
|
17
|
+
* @returns {string} The validated identifier
|
|
18
|
+
* @throws {Error} If the identifier is invalid
|
|
19
|
+
*/
|
|
20
|
+
validateId(name) {
|
|
21
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
22
|
+
throw new Error('Identifier must be a non-empty string')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (name.length > 128) {
|
|
26
|
+
throw new Error(`Identifier too long: ${name.slice(0, 20)}...`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid identifier "${name}". ` +
|
|
32
|
+
'Identifiers must contain only letters, numbers, and underscores, ' +
|
|
33
|
+
'and must start with a letter or underscore.'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return name
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Escape an identifier for safe use in SQL.
|
|
42
|
+
* Validates first, then wraps in quotes with proper escaping.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} name - The identifier to escape
|
|
45
|
+
* @returns {string} The escaped identifier
|
|
46
|
+
*/
|
|
47
|
+
escapeId(name) {
|
|
48
|
+
// Validate first
|
|
49
|
+
this.validateId(name)
|
|
50
|
+
|
|
51
|
+
// MySQL/PlanetScale uses backticks for identifiers
|
|
52
|
+
if (this.driver === 'planetscale' || this.driver === 'mysql') {
|
|
53
|
+
return `\`${name.replace(/`/g, '``')}\``
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// SQLite, Postgres, Turso, Neon use double quotes for identifiers
|
|
57
|
+
return `"${name.replace(/"/g, '""')}"`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the placeholder syntax for the current driver.
|
|
62
|
+
*
|
|
63
|
+
* @param {number} index - Zero-based parameter index
|
|
64
|
+
* @returns {string} The placeholder string
|
|
65
|
+
*/
|
|
66
|
+
placeholder(index) {
|
|
67
|
+
// Postgres and Neon use $1, $2, etc.
|
|
68
|
+
if (this.driver === 'postgres' || this.driver === 'pg' || this.driver === 'neon') {
|
|
69
|
+
return `$${index + 1}`
|
|
70
|
+
}
|
|
71
|
+
// SQLite, Turso, PlanetScale use ?
|
|
72
|
+
return '?'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate an operator for WHERE clauses.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} operator - The operator to validate
|
|
79
|
+
* @returns {string} The validated operator
|
|
80
|
+
* @throws {Error} If the operator is not allowed
|
|
81
|
+
*/
|
|
82
|
+
validateOperator(operator) {
|
|
83
|
+
const allowed = ['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT']
|
|
84
|
+
const upper = operator.toUpperCase()
|
|
85
|
+
|
|
86
|
+
if (!allowed.includes(upper)) {
|
|
87
|
+
throw new Error(`Invalid operator: ${operator}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return upper
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate sort direction.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} direction - ASC or DESC
|
|
97
|
+
* @returns {string} The validated direction
|
|
98
|
+
*/
|
|
99
|
+
validateDirection(direction) {
|
|
100
|
+
const upper = (direction || 'ASC').toUpperCase()
|
|
101
|
+
|
|
102
|
+
if (upper !== 'ASC' && upper !== 'DESC') {
|
|
103
|
+
throw new Error(`Invalid sort direction: ${direction}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return upper
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a column list for SELECT.
|
|
111
|
+
*
|
|
112
|
+
* @param {string[]} columns - Array of column names
|
|
113
|
+
* @returns {string} Escaped column list
|
|
114
|
+
*/
|
|
115
|
+
columnList(columns) {
|
|
116
|
+
if (!columns || columns.length === 0) {
|
|
117
|
+
return '*'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return columns.map(col => {
|
|
121
|
+
if (col === '*') return '*'
|
|
122
|
+
return this.escapeId(col)
|
|
123
|
+
}).join(', ')
|
|
124
|
+
}
|
|
125
|
+
}
|