@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,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
+ }