vasp-cli 0.3.1 → 0.9.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 (140) hide show
  1. package/dist/vasp +80 -31
  2. package/package.json +2 -2
  3. package/starters/minimal.vasp +1 -1
  4. package/starters/recipe.vasp +70 -0
  5. package/starters/todo-auth-ssr.vasp +33 -20
  6. package/starters/todo.vasp +15 -8
  7. package/templates/shared/.gitignore.hbs +1 -0
  8. package/templates/shared/README.md.hbs +53 -0
  9. package/templates/shared/auth/client/Login.vue.hbs +1 -1
  10. package/templates/shared/auth/client/Register.vue.hbs +1 -1
  11. package/templates/shared/auth/server/index.hbs +4 -8
  12. package/templates/shared/auth/server/middleware.hbs +33 -15
  13. package/templates/shared/auth/server/plugin.hbs +7 -0
  14. package/templates/shared/auth/server/providers/github.hbs +1 -1
  15. package/templates/shared/auth/server/providers/google.hbs +1 -1
  16. package/templates/shared/auth/server/providers/usernameAndPassword.hbs +3 -6
  17. package/templates/shared/bunfig.toml.hbs +3 -0
  18. package/templates/shared/drizzle/schema.hbs +39 -9
  19. package/templates/shared/jobs/_job.hbs +12 -2
  20. package/templates/shared/package.json.hbs +14 -4
  21. package/templates/shared/server/db/client.hbs +19 -1
  22. package/templates/shared/server/db/seed.hbs +16 -0
  23. package/templates/shared/server/index.hbs +48 -0
  24. package/templates/shared/server/middleware/errorHandler.hbs +75 -0
  25. package/templates/shared/server/middleware/logger.hbs +74 -0
  26. package/templates/{templates/shared → shared}/server/middleware/rateLimit.hbs +2 -2
  27. package/templates/shared/server/routes/_vasp.hbs +37 -0
  28. package/templates/shared/server/routes/actions/_action.hbs +5 -1
  29. package/templates/shared/server/routes/api/_api.hbs +24 -0
  30. package/templates/shared/server/routes/crud/_crud.hbs +103 -11
  31. package/templates/shared/server/routes/queries/_query.hbs +5 -1
  32. package/templates/shared/server/routes/realtime/_channel.hbs +58 -10
  33. package/templates/shared/shared/types.hbs +58 -0
  34. package/templates/shared/shared/validation.hbs +20 -0
  35. package/templates/shared/tests/actions/_action.test.js.hbs +7 -0
  36. package/templates/shared/tests/actions/_action.test.ts.hbs +7 -0
  37. package/templates/shared/tests/auth/login.test.js.hbs +7 -0
  38. package/templates/shared/tests/auth/login.test.ts.hbs +7 -0
  39. package/templates/shared/tests/crud/_entity.test.js.hbs +7 -0
  40. package/templates/shared/tests/crud/_entity.test.ts.hbs +7 -0
  41. package/templates/shared/tests/queries/_query.test.js.hbs +7 -0
  42. package/templates/shared/tests/queries/_query.test.ts.hbs +7 -0
  43. package/templates/shared/tests/setup.js.hbs +5 -0
  44. package/templates/shared/tests/setup.ts.hbs +5 -0
  45. package/templates/shared/tests/vitest.config.js.hbs +8 -0
  46. package/templates/shared/tests/vitest.config.ts.hbs +8 -0
  47. package/templates/shared/tsconfig.json.hbs +2 -1
  48. package/templates/spa/js/src/App.vue.hbs +9 -1
  49. package/templates/spa/js/src/components/VaspErrorBoundary.vue.hbs +33 -0
  50. package/templates/spa/js/src/components/VaspNotifications.vue.hbs +60 -0
  51. package/templates/spa/js/src/vasp/auth.js.hbs +31 -15
  52. package/templates/spa/js/src/vasp/client/actions.js.hbs +7 -1
  53. package/templates/spa/js/src/vasp/client/crud.js.hbs +94 -5
  54. package/templates/spa/js/src/vasp/useVaspNotifications.js.hbs +35 -0
  55. package/templates/spa/js/vite.config.js.hbs +1 -0
  56. package/templates/spa/ts/src/App.vue.hbs +9 -1
  57. package/templates/spa/ts/src/components/VaspErrorBoundary.vue.hbs +33 -0
  58. package/templates/spa/ts/src/components/VaspNotifications.vue.hbs +60 -0
  59. package/templates/spa/ts/src/vasp/auth.ts.hbs +31 -15
  60. package/templates/spa/ts/src/vasp/client/actions.ts.hbs +7 -1
  61. package/templates/spa/ts/src/vasp/client/crud.ts.hbs +96 -10
  62. package/templates/spa/ts/src/vasp/client/types.ts.hbs +14 -28
  63. package/templates/spa/ts/src/vasp/useVaspNotifications.ts.hbs +41 -0
  64. package/templates/spa/ts/vite.config.ts.hbs +1 -0
  65. package/templates/ssr/js/error.vue.hbs +23 -0
  66. package/templates/ssr/js/nuxt.config.js.hbs +1 -0
  67. package/templates/ssr/js/plugins/vasp.client.js.hbs +11 -1
  68. package/templates/ssr/ts/error.vue.hbs +26 -0
  69. package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
  70. package/templates/ssr/ts/plugins/vasp.client.ts.hbs +11 -1
  71. package/templates/starters/minimal.vasp +15 -0
  72. package/templates/starters/recipe.vasp +70 -0
  73. package/templates/starters/todo-auth-ssr.vasp +65 -0
  74. package/templates/starters/todo.vasp +42 -0
  75. package/templates/templates/shared/.gitignore.hbs +0 -8
  76. package/templates/templates/shared/auth/client/Login.vue.hbs +0 -46
  77. package/templates/templates/shared/auth/client/Register.vue.hbs +0 -42
  78. package/templates/templates/shared/auth/server/index.hbs +0 -51
  79. package/templates/templates/shared/auth/server/middleware.hbs +0 -33
  80. package/templates/templates/shared/auth/server/providers/github.hbs +0 -48
  81. package/templates/templates/shared/auth/server/providers/google.hbs +0 -53
  82. package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +0 -69
  83. package/templates/templates/shared/bunfig.toml.hbs +0 -2
  84. package/templates/templates/shared/drizzle/schema.hbs +0 -48
  85. package/templates/templates/shared/jobs/_job.hbs +0 -34
  86. package/templates/templates/shared/jobs/boss.hbs +0 -15
  87. package/templates/templates/shared/package.json.hbs +0 -35
  88. package/templates/templates/shared/server/db/client.hbs +0 -12
  89. package/templates/templates/shared/server/index.hbs +0 -60
  90. package/templates/templates/shared/server/routes/actions/_action.hbs +0 -20
  91. package/templates/templates/shared/server/routes/crud/_crud.hbs +0 -86
  92. package/templates/templates/shared/server/routes/jobs/_schedule.hbs +0 -12
  93. package/templates/templates/shared/server/routes/queries/_query.hbs +0 -20
  94. package/templates/templates/shared/server/routes/realtime/_channel.hbs +0 -78
  95. package/templates/templates/shared/server/routes/realtime/index.hbs +0 -9
  96. package/templates/templates/shared/tsconfig.json.hbs +0 -21
  97. package/templates/templates/spa/js/index.html.hbs +0 -12
  98. package/templates/templates/spa/js/src/App.vue.hbs +0 -3
  99. package/templates/templates/spa/js/src/main.js.hbs +0 -9
  100. package/templates/templates/spa/js/src/router/index.js.hbs +0 -41
  101. package/templates/templates/spa/js/src/vasp/auth.js.hbs +0 -45
  102. package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +0 -15
  103. package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +0 -30
  104. package/templates/templates/spa/js/src/vasp/client/index.js.hbs +0 -16
  105. package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +0 -15
  106. package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +0 -51
  107. package/templates/templates/spa/js/src/vasp/plugin.js.hbs +0 -11
  108. package/templates/templates/spa/js/vite.config.js.hbs +0 -26
  109. package/templates/templates/spa/ts/index.html.hbs +0 -12
  110. package/templates/templates/spa/ts/src/App.vue.hbs +0 -3
  111. package/templates/templates/spa/ts/src/main.ts.hbs +0 -9
  112. package/templates/templates/spa/ts/src/router/index.ts.hbs +0 -41
  113. package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +0 -53
  114. package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +0 -19
  115. package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +0 -37
  116. package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +0 -17
  117. package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +0 -19
  118. package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +0 -56
  119. package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +0 -33
  120. package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +0 -12
  121. package/templates/templates/spa/ts/vite.config.ts.hbs +0 -26
  122. package/templates/templates/ssr/js/_page.vue.hbs +0 -10
  123. package/templates/templates/ssr/js/app.vue.hbs +0 -3
  124. package/templates/templates/ssr/js/composables/useAuth.js.hbs +0 -52
  125. package/templates/templates/ssr/js/composables/useVasp.js.hbs +0 -6
  126. package/templates/templates/ssr/js/middleware/auth.js.hbs +0 -8
  127. package/templates/templates/ssr/js/nuxt.config.js.hbs +0 -15
  128. package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +0 -27
  129. package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +0 -33
  130. package/templates/templates/ssr/ts/_page.vue.hbs +0 -10
  131. package/templates/templates/ssr/ts/app.vue.hbs +0 -3
  132. package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +0 -56
  133. package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +0 -10
  134. package/templates/templates/ssr/ts/middleware/auth.ts.hbs +0 -8
  135. package/templates/templates/ssr/ts/nuxt.config.ts.hbs +0 -19
  136. package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +0 -27
  137. package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +0 -33
  138. /package/templates/{templates/shared/.env.example.hbs → shared/.env.hbs} +0 -0
  139. /package/templates/{templates/shared → shared}/drizzle/drizzle.config.hbs +0 -0
  140. /package/templates/{templates/shared → shared}/server/middleware/csrf.hbs +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasp-cli",
3
- "version": "0.3.1",
3
+ "version": "0.9.0",
4
4
  "description": "The Vasp CLI — declarative full-stack framework for Vue developers",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "build": "bun build ./bin/vasp.ts --target=bun --outfile=dist/vasp --minify --banner='#!/usr/bin/env bun' && chmod +x dist/vasp && cp -r ../../templates ./templates",
17
+ "build": "rm -rf dist templates starters && bun build ./bin/vasp.ts --target=bun --outfile=dist/vasp --minify --banner='#!/usr/bin/env bun' && chmod +x dist/vasp && mkdir -p templates starters && cp -R ../../templates/. ./templates && cp -R ../../templates/starters/. ./starters",
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
@@ -1,5 +1,5 @@
1
1
  app MinimalApp {
2
- title: "My Vasp App"
2
+ title: "Minimal App"
3
3
  db: Drizzle
4
4
  ssr: false
5
5
  typescript: false
@@ -0,0 +1,70 @@
1
+ app RecipeApp {
2
+ title: "Recipe App"
3
+ db: Drizzle
4
+ ssr: false
5
+ typescript: false
6
+ }
7
+
8
+ auth RecipeAuth {
9
+ userEntity: User
10
+ methods: [usernameAndPassword]
11
+ }
12
+
13
+ entity Recipe {
14
+ id: Int @id
15
+ title: String
16
+ description: String
17
+ ingredients: String
18
+ instructions: String
19
+ author: String
20
+ }
21
+
22
+ route HomeRoute {
23
+ path: "/"
24
+ to: HomePage
25
+ }
26
+
27
+ route RecipesRoute {
28
+ path: "/recipes"
29
+ to: RecipesPage
30
+ }
31
+
32
+ route AddRecipeRoute {
33
+ path: "/recipes/new"
34
+ to: AddRecipePage
35
+ }
36
+
37
+ page HomePage {
38
+ component: import HomePage from "@src/pages/HomePage.vue"
39
+ }
40
+
41
+ page RecipesPage {
42
+ component: import RecipesPage from "@src/pages/RecipesPage.vue"
43
+ }
44
+
45
+ page AddRecipePage {
46
+ component: import AddRecipePage from "@src/pages/AddRecipePage.vue"
47
+ }
48
+
49
+ query getRecipes {
50
+ fn: import { getRecipes } from "@src/queries.js"
51
+ entities: [Recipe]
52
+ auth: true
53
+ }
54
+
55
+ action createRecipe {
56
+ fn: import { createRecipe } from "@src/actions.js"
57
+ entities: [Recipe]
58
+ auth: true
59
+ }
60
+
61
+ action deleteRecipe {
62
+ fn: import { deleteRecipe } from "@src/actions.js"
63
+ entities: [Recipe]
64
+ auth: true
65
+ }
66
+
67
+ crud Recipe {
68
+ entity: Recipe
69
+ operations: [list, create, update, delete]
70
+ }
@@ -1,52 +1,65 @@
1
- app TodoSsrApp {
2
- title: "Todo App (SSR + Auth)"
1
+ app TodoAuthSsrApp {
2
+ title: "Todo App"
3
3
  db: Drizzle
4
4
  ssr: true
5
- typescript: true
5
+ typescript: false
6
6
  }
7
7
 
8
- auth UserAuth {
8
+ auth TodoAuth {
9
9
  userEntity: User
10
10
  methods: [usernameAndPassword]
11
11
  }
12
12
 
13
- route HomeRoute {
14
- path: "/"
15
- to: TodoPage
13
+ entity User {
14
+ id: Int @id
15
+ username: String @unique
16
+ email: String @unique
16
17
  }
17
18
 
18
- route DashboardRoute {
19
- path: "/dashboard"
20
- to: DashboardPage
19
+ entity Todo {
20
+ id: Int @id
21
+ title: String
22
+ done: Boolean
23
+ createdAt: DateTime @default(now)
21
24
  }
22
25
 
23
- page TodoPage {
24
- component: import TodoPage from "@src/pages/TodoPage.vue"
26
+ route HomeRoute {
27
+ path: "/"
28
+ to: HomePage
25
29
  }
26
30
 
27
- page DashboardPage {
28
- component: import DashboardPage from "@src/pages/DashboardPage.vue"
31
+ route TodoRoute {
32
+ path: "/todos"
33
+ to: TodoPage
29
34
  }
30
35
 
31
- crud Todo {
32
- entity: Todo
33
- operations: [list, create, update, delete]
36
+ page HomePage {
37
+ component: import Home from "@src/pages/Home.vue"
38
+ }
39
+
40
+ page TodoPage {
41
+ component: import TodoList from "@src/pages/TodoList.vue"
34
42
  }
35
43
 
36
44
  query getTodos {
37
- fn: import { getTodos } from "@src/queries.ts"
45
+ fn: import { getTodos } from "@src/queries.js"
38
46
  entities: [Todo]
39
47
  auth: true
40
48
  }
41
49
 
42
50
  action createTodo {
43
- fn: import { createTodo } from "@src/actions.ts"
51
+ fn: import { createTodo } from "@src/actions.js"
44
52
  entities: [Todo]
45
53
  auth: true
46
54
  }
47
55
 
48
56
  action deleteTodo {
49
- fn: import { deleteTodo } from "@src/actions.ts"
57
+ fn: import { deleteTodo } from "@src/actions.js"
50
58
  entities: [Todo]
51
59
  auth: true
52
60
  }
61
+
62
+ crud Todo {
63
+ entity: Todo
64
+ operations: [list, create, update, delete]
65
+ }
@@ -5,18 +5,20 @@ app TodoApp {
5
5
  typescript: false
6
6
  }
7
7
 
8
- route HomeRoute {
9
- path: "/"
10
- to: TodoPage
8
+ entity Todo {
9
+ id: Int @id
10
+ title: String
11
+ done: Boolean
12
+ createdAt: DateTime @default(now)
11
13
  }
12
14
 
13
- page TodoPage {
14
- component: import TodoPage from "@src/pages/TodoPage.vue"
15
+ route HomeRoute {
16
+ path: "/"
17
+ to: HomePage
15
18
  }
16
19
 
17
- crud Todo {
18
- entity: Todo
19
- operations: [list, create, update, delete]
20
+ page HomePage {
21
+ component: import Home from "@src/pages/Home.vue"
20
22
  }
21
23
 
22
24
  query getTodos {
@@ -33,3 +35,8 @@ action deleteTodo {
33
35
  fn: import { deleteTodo } from "@src/actions.js"
34
36
  entities: [Todo]
35
37
  }
38
+
39
+ crud Todo {
40
+ entity: Todo
41
+ operations: [list, create, update, delete]
42
+ }
@@ -2,6 +2,7 @@ node_modules
2
2
  dist
3
3
  .output
4
4
  .nuxt
5
+ .vasp
5
6
  .vasp-gen
6
7
  .env
7
8
  .env.local
@@ -0,0 +1,53 @@
1
+ # {{appTitle}}
2
+
3
+ A full-stack app built with [Vasp](https://github.com/AliBeigi/Vasp).
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ bun install
10
+
11
+ # Set up your database
12
+ # Make sure PostgreSQL is running, then push the schema:
13
+ bun run db:push
14
+
15
+ # Start the dev server (backend + frontend)
16
+ vasp start
17
+ ```
18
+
19
+ ## Project Structure
20
+
21
+ ```
22
+ main.vasp # Vasp declarative config
23
+ server/ # Elysia backend
24
+ routes/ # API routes (queries, actions, CRUD)
25
+ db/ # Drizzle DB client
26
+ middleware/ # Rate limiting{{#if hasAuth}}, auth{{/if}}
27
+ {{#if hasJobs}} jobs/ # Background jobs (PgBoss)
28
+ {{/if}}src/ # Frontend source
29
+ pages/ # Vue page components
30
+ components/ # Shared components
31
+ drizzle/ # Database schema & migrations
32
+ ```
33
+
34
+ ## Scripts
35
+
36
+ | Command | Description |
37
+ |---------|-------------|
38
+ | `vasp start` | Start dev server (backend + frontend) |
39
+ | `vasp build` | Production build |
40
+ | `bun run db:push` | Push schema to database |
41
+ | `bun run db:generate` | Generate a migration |
42
+ | `bun run db:migrate` | Run migrations |
43
+ | `bun run db:studio` | Open Drizzle Studio |
44
+
45
+ ## Environment Variables
46
+
47
+ Copy `.env.example` to `.env` and update the values:
48
+
49
+ - `DATABASE_URL` — PostgreSQL connection string
50
+ - `PORT` — Backend server port (default: {{backendPort}})
51
+ - `VITE_API_URL` — Frontend API base URL
52
+ {{#if hasAuth}}- `JWT_SECRET` — Secret for JWT token signing
53
+ {{/if}}
@@ -38,7 +38,7 @@ async function handleLogin() {
38
38
  await login(username.value, password.value)
39
39
  router.push('/')
40
40
  } catch (err) {
41
- error.value = err?.data?.error || 'Login failed'
41
+ error.value = err?.data?.error || err?.message || 'Login failed'
42
42
  } finally {
43
43
  loading.value = false
44
44
  }
@@ -34,7 +34,7 @@ async function handleRegister() {
34
34
  await register(username.value, password.value, email.value || undefined)
35
35
  router.push('/')
36
36
  } catch (err) {
37
- error.value = err?.data?.error || 'Registration failed'
37
+ error.value = err?.data?.error || err?.message || 'Registration failed'
38
38
  } finally {
39
39
  loading.value = false
40
40
  }
@@ -1,9 +1,9 @@
1
1
  import { Elysia } from 'elysia'
2
- import { jwt } from '@elysiajs/jwt'
3
- import { cookie } from '@elysiajs/cookie'
4
2
  import { db } from '../db/client.{{ext}}'
5
3
  import { users } from '../../drizzle/schema.{{ext}}'
6
4
  import { eq } from 'drizzle-orm'
5
+ import { authPlugin } from './plugin.{{ext}}'
6
+ import { VaspError } from '../middleware/errorHandler.{{ext}}'
7
7
  {{#if (includes authMethods "usernameAndPassword")}}
8
8
  import { usernameAndPasswordRoutes } from './providers/usernameAndPassword.{{ext}}'
9
9
  {{/if}}
@@ -14,13 +14,9 @@ import { googleRoutes } from './providers/google.{{ext}}'
14
14
  import { githubRoutes } from './providers/github.{{ext}}'
15
15
  {{/if}}
16
16
 
17
- const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
17
+ export { authPlugin } from './plugin.{{ext}}'
18
18
 
19
- export const authPlugin = new Elysia({ name: 'auth-plugin' })
20
- .use(jwt({ name: 'jwt', secret: JWT_SECRET }))
21
- .use(cookie())
22
-
23
- export const authRoutes = new Elysia({ prefix: '/auth' })
19
+ export const authRoutes = new Elysia({ prefix: '/api/auth' })
24
20
  .use(authPlugin)
25
21
  {{#if (includes authMethods "usernameAndPassword")}}
26
22
  .use(usernameAndPasswordRoutes)
@@ -1,11 +1,11 @@
1
1
  import { Elysia } from 'elysia'
2
- import { jwt } from '@elysiajs/jwt'
3
- import { cookie } from '@elysiajs/cookie'
2
+ import { jwtVerify } from 'jose'
4
3
  import { db } from '../db/client.{{ext}}'
5
4
  import { users } from '../../drizzle/schema.{{ext}}'
6
5
  import { eq } from 'drizzle-orm'
6
+ import { VaspError } from '../middleware/errorHandler.{{ext}}'
7
7
 
8
- const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
8
+ const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET || 'change-me-in-production')
9
9
 
10
10
  /**
11
11
  * requireAuth — Elysia plugin that verifies the JWT cookie and injects `user` into the context.
@@ -15,19 +15,37 @@ const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
15
15
  * new Elysia().use(requireAuth).get('/protected', ({ user }) => user)
16
16
  */
17
17
  export const requireAuth = new Elysia({ name: 'require-auth' })
18
- .use(jwt({ name: 'jwt', secret: JWT_SECRET }))
19
- .use(cookie())
20
- .derive(async ({ jwt, cookie: { token }, set }) => {
21
- const payload = await jwt.verify(token?.value ?? '')
22
- if (!payload || typeof payload.userId !== 'number') {
23
- set.status = 401
24
- throw new Error('Unauthorized')
18
+ .resolve({ as: 'scoped' }, async ({ cookie }) => {
19
+ const tokenValue = cookie?.token?.value ?? ''
20
+ if (!tokenValue) return { user: null }
21
+ try {
22
+ const { payload } = await jwtVerify(tokenValue, JWT_SECRET)
23
+ if (!payload || typeof payload.userId !== 'number') {
24
+ return { user: null }
25
+ }
26
+ const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
27
+ if (!user) {
28
+ return { user: null }
29
+ }
30
+ const { passwordHash: _ph, ...safeUser } = user
31
+ return { user: safeUser }
32
+ } catch {
33
+ return { user: null }
25
34
  }
26
- const [user] = await db.select().from(users).where(eq(users.id, payload.userId)).limit(1)
35
+ })
36
+ .onBeforeHandle({ as: 'scoped' }, ({ user }) => {
27
37
  if (!user) {
28
- set.status = 401
29
- throw new Error('User not found')
38
+ throw new VaspError('AUTH_REQUIRED', 'Authentication required', 401)
30
39
  }
31
- const { passwordHash: _ph, ...safeUser } = user
32
- return { user: safeUser }
33
40
  })
41
+
42
+ export function requireRole(roles{{#if isTypeScript}}: string[]{{/if}}) {
43
+ return new Elysia({ name: 'require-role' })
44
+ .use(requireAuth)
45
+ .onBeforeHandle({ as: 'scoped' }, ({ user }) => {
46
+ const userRole = typeof user?.role === 'string' ? user.role : ''
47
+ if (!roles.includes(userRole)) {
48
+ throw new VaspError('AUTH_FORBIDDEN', 'Insufficient permissions', 403)
49
+ }
50
+ })
51
+ }
@@ -0,0 +1,7 @@
1
+ import { Elysia } from 'elysia'
2
+ import { jwt } from '@elysiajs/jwt'
3
+
4
+ const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
5
+
6
+ export const authPlugin = new Elysia({ name: 'auth-plugin' })
7
+ .use(jwt({ name: 'jwt', secret: JWT_SECRET }))
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia'
2
2
  import { db } from '../../db/client.{{ext}}'
3
3
  import { users } from '../../../drizzle/schema.{{ext}}'
4
4
  import { eq } from 'drizzle-orm'
5
- import { authPlugin } from '../index.{{ext}}'
5
+ import { authPlugin } from '../plugin.{{ext}}'
6
6
 
7
7
  const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''
8
8
  const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia'
2
2
  import { db } from '../../db/client.{{ext}}'
3
3
  import { users } from '../../../drizzle/schema.{{ext}}'
4
4
  import { eq } from 'drizzle-orm'
5
- import { authPlugin } from '../index.{{ext}}'
5
+ import { authPlugin } from '../plugin.{{ext}}'
6
6
 
7
7
  const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
8
8
  const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''
@@ -2,17 +2,14 @@ import { Elysia, t } from 'elysia'
2
2
  import { db } from '../../db/client.{{ext}}'
3
3
  import { users } from '../../../drizzle/schema.{{ext}}'
4
4
  import { eq } from 'drizzle-orm'
5
- import { authPlugin } from '../index.{{ext}}'
5
+ import { authPlugin } from '../plugin.{{ext}}'
6
6
 
7
7
  async function hashPassword(password{{#if isTypeScript}}: string{{/if}}) {
8
- const encoder = new TextEncoder()
9
- const data = encoder.encode(password)
10
- const hash = await crypto.subtle.digest('SHA-256', data)
11
- return Buffer.from(hash).toString('hex')
8
+ return Bun.password.hash(password, 'argon2id')
12
9
  }
13
10
 
14
11
  async function verifyPassword(password{{#if isTypeScript}}: string{{/if}}, hash{{#if isTypeScript}}: string{{/if}}) {
15
- return (await hashPassword(password)) === hash
12
+ return Bun.password.verify(password, hash)
16
13
  }
17
14
 
18
15
  export const usernameAndPasswordRoutes = new Elysia()
@@ -1,2 +1,5 @@
1
1
  [install]
2
2
  exact = false
3
+
4
+ [resolve]
5
+ "@src" = "./src"
@@ -1,17 +1,25 @@
1
- import { pgTable, serial, text, timestamp, boolean } from 'drizzle-orm/pg-core'
1
+ import { pgTable, text, integer, boolean, timestamp, doublePrecision, jsonb{{#if hasAnyRelations}}, relations{{/if}}{{#if hasEnums}}, pgEnum{{/if}} } from 'drizzle-orm/pg-core'
2
2
  {{#if isTypeScript}}
3
3
  import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'
4
4
  {{/if}}
5
5
 
6
+ {{#if hasEnums}}
7
+ {{#each enumDeclarations}}
8
+ export const {{fnName}} = pgEnum('{{dbName}}', [{{#each values}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}])
9
+ {{/each}}
10
+ {{/if}}
6
11
  {{#if hasAuth}}
7
12
  // Users table — generated by Vasp auth system
8
13
  export const users = pgTable('users', {
9
- id: serial('id').primaryKey(),
14
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
10
15
  username: text('username').notNull().unique(),
11
16
  email: text('email').unique(),
12
17
  passwordHash: text('password_hash'),
13
18
  googleId: text('google_id').unique(),
14
19
  githubId: text('github_id').unique(),
20
+ {{#each authUserExtraFields}}
21
+ {{camelCase name}}: {{{drizzleColumn name type modifiers nullable defaultValue isUpdatedAt}}},
22
+ {{/each}}
15
23
  createdAt: timestamp('created_at').defaultNow().notNull(),
16
24
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
17
25
  })
@@ -21,17 +29,39 @@ export type NewUser = InferInsertModel<typeof users>
21
29
  {{/if}}
22
30
 
23
31
  {{/if}}
24
- {{#each cruds}}
25
- // {{entity}} table — add your columns below
26
- export const {{camelCase entity}}s = pgTable('{{camelCase entity}}s', {
27
- id: serial('id').primaryKey(),
28
- // TODO: Add your {{entity}} columns here
32
+ {{#each entitiesWithSchema}}
33
+ // {{name}} table — generated from entity block
34
+ export const {{camelCase name}}s = pgTable('{{camelCase name}}s', {
35
+ {{#each scalarFields}}
36
+ {{#if isForeignKey}}
37
+ {{camelCase name}}: integer('{{camelCase name}}').notNull().references(() => {{referencedTable}}.id, { onDelete: '{{onDelete}}' }),
38
+ {{else}}
39
+ {{#if isEnum}}
40
+ {{camelCase name}}: {{enumFnName}}('{{camelCase name}}'){{#unless nullable}}.notNull(){{/unless}},
41
+ {{else}}
42
+ {{camelCase name}}: {{{drizzleColumn name type modifiers nullable defaultValue isUpdatedAt}}},
43
+ {{/if}}
44
+ {{/if}}
45
+ {{/each}}
29
46
  createdAt: timestamp('created_at').defaultNow().notNull(),
30
47
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
31
48
  })
32
49
  {{#if ../isTypeScript}}
33
- export type {{pascalCase entity}} = InferSelectModel<typeof {{camelCase entity}}s>
34
- export type New{{pascalCase entity}} = InferInsertModel<typeof {{camelCase entity}}s>
50
+ export type {{pascalCase name}} = InferSelectModel<typeof {{camelCase name}}s>
51
+ export type New{{pascalCase name}} = InferInsertModel<typeof {{camelCase name}}s>
35
52
  {{/if}}
36
53
 
37
54
  {{/each}}
55
+ {{#each entitiesWithSchema}}
56
+ {{#if hasRelations}}
57
+ export const {{camelCase name}}sRelations = relations({{camelCase name}}s, ({ one, many }) => ({
58
+ {{#each manyToOne}}
59
+ {{camelCase name}}: one({{relatedTable}}, { fields: [{{camelCase ../name}}s.{{camelCase localField}}], references: [{{relatedTable}}.id] }),
60
+ {{/each}}
61
+ {{#each oneToMany}}
62
+ {{camelCase fieldName}}: many({{relatedTable}}),
63
+ {{/each}}
64
+ }))
65
+
66
+ {{/if}}
67
+ {{/each}}
@@ -12,13 +12,23 @@ export async function register{{pascalCase name}}Worker() {
12
12
  await boss.work(JOB_NAME, async (job) => {
13
13
  await {{namedExport}}(job.data)
14
14
  })
15
+ {{#if hasSchedule}}
16
+
17
+ // Register cron schedule: {{schedule}}
18
+ await boss.schedule(JOB_NAME, '{{schedule}}', {})
19
+ {{/if}}
15
20
  }
16
21
 
17
22
  /**
18
- * Schedule a '{{name}}' job.
23
+ * Schedule a '{{name}}' job on demand.
19
24
  * @param {unknown} data - Data to pass to the job handler
20
25
  */
21
26
  export async function schedule{{pascalCase name}}(data) {
22
27
  const boss = await getBoss()
23
- return boss.send(JOB_NAME, data)
28
+ return boss.send(JOB_NAME, data, {
29
+ retryLimit: Number(process.env.JOB_RETRY_LIMIT) || 3,
30
+ retryDelay: Number(process.env.JOB_RETRY_DELAY) || 60,
31
+ retryBackoff: true,
32
+ expireInMinutes: Number(process.env.JOB_EXPIRE_MINUTES) || 15,
33
+ })
24
34
  }
@@ -6,14 +6,23 @@
6
6
  "scripts": {
7
7
  "dev": "vasp start",
8
8
  "build": "vasp build",
9
+ "test": "vitest run",
9
10
  "dev:server": "bun --hot server/index.{{ext}}",
10
- "dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}"
11
+ "dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}",
12
+ "db:generate": "bunx drizzle-kit generate",
13
+ "db:migrate": "bunx drizzle-kit migrate",
14
+ "db:push": "bunx drizzle-kit push",
15
+ "db:studio": "bunx drizzle-kit studio"{{#if seed}},
16
+ "db:seed": "bun server/db/seed.{{ext}}"{{/if}}
11
17
  },
12
18
  "dependencies": {
13
- "@vasp-framework/runtime": "^0.1.0",
19
+ "@vasp-framework/runtime": "^0.4.0",
14
20
  "elysia": "^1.1.0",
15
21
  "@elysiajs/cors": "^1.1.0",
16
- "@elysiajs/static": "^1.1.0",
22
+ "@elysiajs/static": "^1.1.0",{{#if auth}}
23
+ "@elysiajs/jwt": "^1.1.0",
24
+ "jose": "^5.0.0",{{/if}}
25
+ "valibot": "^1.0.0",
17
26
  "drizzle-orm": "^0.36.0",
18
27
  "postgres": "^3.4.0",
19
28
  "ofetch": "^1.3.4",
@@ -23,7 +32,8 @@
23
32
  "pg-boss": "^10.0.0"{{/if}}
24
33
  },
25
34
  "devDependencies": {
26
- "drizzle-kit": "^0.28.0"{{#if isSpa}},
35
+ "drizzle-kit": "^0.28.0",
36
+ "vitest": "^2.1.9"{{#if isSpa}},
27
37
  "@vitejs/plugin-vue": "^5.2.0",
28
38
  "vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
29
39
  "typescript": "^5.6.0",
@@ -5,8 +5,26 @@ import * as schema from '../../drizzle/schema.{{ext}}'
5
5
  const connectionString = process.env.DATABASE_URL
6
6
 
7
7
  if (!connectionString) {
8
- throw new Error('DATABASE_URL environment variable is required')
8
+ console.error('\n✗ DATABASE_URL environment variable is required. Set it in .env\n')
9
+ process.exit(1)
10
+ }
11
+
12
+ if (connectionString.startsWith('postgres://user:password@localhost')) {
13
+ console.warn(
14
+ '\n⚠ DATABASE_URL looks like a placeholder. Edit .env to set your real database credentials.\n'
15
+ )
9
16
  }
10
17
 
11
18
  const client = postgres(connectionString)
19
+
20
+ // Verify database connectivity at startup
21
+ try {
22
+ await client`SELECT 1`
23
+ } catch (err) {
24
+ const message = err instanceof Error ? err.message : String(err)
25
+ console.error('\n✗ Cannot connect to database. Verify DATABASE_URL in .env')
26
+ console.error(` ${message}\n`)
27
+ process.exit(1)
28
+ }
29
+
12
30
  export const db = drizzle(client, { schema })
@@ -0,0 +1,16 @@
1
+ import { db } from './client.{{ext}}'
2
+ {{#if (eq seedImportKind "default")}}
3
+ import {{seedImportName}} from '{{importPath fnSource ext}}'
4
+ {{else}}
5
+ import { {{seedImportName}} } from '{{importPath fnSource ext}}'
6
+ {{/if}}
7
+
8
+ async function runSeed() {
9
+ await {{seedImportName}}({ db })
10
+ console.log('✅ Seed completed')
11
+ }
12
+
13
+ runSeed().catch((error) => {
14
+ console.error('❌ Seed failed', error)
15
+ process.exit(1)
16
+ })