vasp-cli 0.4.0 → 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 (136) hide show
  1. package/dist/vasp +82 -34
  2. package/package.json +2 -2
  3. package/starters/minimal.vasp +1 -1
  4. package/starters/recipe.vasp +11 -20
  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/auth/server/index.hbs +4 -8
  9. package/templates/shared/auth/server/middleware.hbs +33 -15
  10. package/templates/{templates/shared → shared}/auth/server/plugin.hbs +0 -2
  11. package/templates/shared/auth/server/providers/github.hbs +1 -1
  12. package/templates/shared/auth/server/providers/google.hbs +1 -1
  13. package/templates/shared/auth/server/providers/usernameAndPassword.hbs +3 -6
  14. package/templates/shared/bunfig.toml.hbs +3 -0
  15. package/templates/shared/drizzle/schema.hbs +38 -19
  16. package/templates/shared/package.json.hbs +11 -4
  17. package/templates/shared/server/db/client.hbs +19 -1
  18. package/templates/shared/server/db/seed.hbs +16 -0
  19. package/templates/shared/server/index.hbs +40 -0
  20. package/templates/shared/server/middleware/errorHandler.hbs +75 -0
  21. package/templates/shared/server/middleware/logger.hbs +74 -0
  22. package/templates/shared/server/middleware/rateLimit.hbs +2 -2
  23. package/templates/shared/server/routes/_vasp.hbs +37 -0
  24. package/templates/shared/server/routes/actions/_action.hbs +5 -1
  25. package/templates/shared/server/routes/api/_api.hbs +24 -0
  26. package/templates/shared/server/routes/crud/_crud.hbs +58 -10
  27. package/templates/shared/server/routes/queries/_query.hbs +5 -1
  28. package/templates/shared/shared/types.hbs +58 -0
  29. package/templates/shared/shared/validation.hbs +20 -0
  30. package/templates/shared/tests/actions/_action.test.js.hbs +7 -0
  31. package/templates/shared/tests/actions/_action.test.ts.hbs +7 -0
  32. package/templates/shared/tests/auth/login.test.js.hbs +7 -0
  33. package/templates/shared/tests/auth/login.test.ts.hbs +7 -0
  34. package/templates/shared/tests/crud/_entity.test.js.hbs +7 -0
  35. package/templates/shared/tests/crud/_entity.test.ts.hbs +7 -0
  36. package/templates/shared/tests/queries/_query.test.js.hbs +7 -0
  37. package/templates/shared/tests/queries/_query.test.ts.hbs +7 -0
  38. package/templates/shared/tests/setup.js.hbs +5 -0
  39. package/templates/shared/tests/setup.ts.hbs +5 -0
  40. package/templates/shared/tests/vitest.config.js.hbs +8 -0
  41. package/templates/shared/tests/vitest.config.ts.hbs +8 -0
  42. package/templates/shared/tsconfig.json.hbs +2 -1
  43. package/templates/spa/js/src/App.vue.hbs +9 -1
  44. package/templates/spa/js/src/components/VaspErrorBoundary.vue.hbs +33 -0
  45. package/templates/spa/js/src/components/VaspNotifications.vue.hbs +60 -0
  46. package/templates/spa/js/src/vasp/auth.js.hbs +31 -15
  47. package/templates/spa/js/src/vasp/client/actions.js.hbs +7 -1
  48. package/templates/spa/js/src/vasp/client/crud.js.hbs +94 -5
  49. package/templates/spa/js/src/vasp/useVaspNotifications.js.hbs +35 -0
  50. package/templates/spa/js/vite.config.js.hbs +1 -0
  51. package/templates/spa/ts/src/App.vue.hbs +9 -1
  52. package/templates/spa/ts/src/components/VaspErrorBoundary.vue.hbs +33 -0
  53. package/templates/spa/ts/src/components/VaspNotifications.vue.hbs +60 -0
  54. package/templates/spa/ts/src/vasp/auth.ts.hbs +31 -15
  55. package/templates/spa/ts/src/vasp/client/actions.ts.hbs +7 -1
  56. package/templates/spa/ts/src/vasp/client/crud.ts.hbs +96 -10
  57. package/templates/spa/ts/src/vasp/client/types.ts.hbs +14 -28
  58. package/templates/spa/ts/src/vasp/useVaspNotifications.ts.hbs +41 -0
  59. package/templates/spa/ts/vite.config.ts.hbs +1 -0
  60. package/templates/ssr/js/error.vue.hbs +23 -0
  61. package/templates/ssr/js/nuxt.config.js.hbs +1 -0
  62. package/templates/ssr/ts/error.vue.hbs +26 -0
  63. package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
  64. package/templates/starters/minimal.vasp +15 -0
  65. package/templates/starters/recipe.vasp +70 -0
  66. package/templates/starters/todo-auth-ssr.vasp +65 -0
  67. package/templates/starters/todo.vasp +42 -0
  68. package/templates/templates/shared/.env.example.hbs +0 -14
  69. package/templates/templates/shared/.gitignore.hbs +0 -8
  70. package/templates/templates/shared/auth/client/Login.vue.hbs +0 -46
  71. package/templates/templates/shared/auth/client/Register.vue.hbs +0 -42
  72. package/templates/templates/shared/auth/server/index.hbs +0 -46
  73. package/templates/templates/shared/auth/server/middleware.hbs +0 -33
  74. package/templates/templates/shared/auth/server/providers/github.hbs +0 -48
  75. package/templates/templates/shared/auth/server/providers/google.hbs +0 -53
  76. package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +0 -66
  77. package/templates/templates/shared/bunfig.toml.hbs +0 -5
  78. package/templates/templates/shared/drizzle/drizzle.config.hbs +0 -10
  79. package/templates/templates/shared/drizzle/schema.hbs +0 -48
  80. package/templates/templates/shared/jobs/_job.hbs +0 -34
  81. package/templates/templates/shared/jobs/boss.hbs +0 -15
  82. package/templates/templates/shared/package.json.hbs +0 -38
  83. package/templates/templates/shared/server/db/client.hbs +0 -12
  84. package/templates/templates/shared/server/index.hbs +0 -73
  85. package/templates/templates/shared/server/middleware/csrf.hbs +0 -34
  86. package/templates/templates/shared/server/middleware/rateLimit.hbs +0 -44
  87. package/templates/templates/shared/server/routes/actions/_action.hbs +0 -20
  88. package/templates/templates/shared/server/routes/crud/_crud.hbs +0 -86
  89. package/templates/templates/shared/server/routes/jobs/_schedule.hbs +0 -12
  90. package/templates/templates/shared/server/routes/queries/_query.hbs +0 -20
  91. package/templates/templates/shared/server/routes/realtime/_channel.hbs +0 -78
  92. package/templates/templates/shared/server/routes/realtime/index.hbs +0 -9
  93. package/templates/templates/shared/tsconfig.json.hbs +0 -21
  94. package/templates/templates/spa/js/index.html.hbs +0 -12
  95. package/templates/templates/spa/js/src/App.vue.hbs +0 -3
  96. package/templates/templates/spa/js/src/main.js.hbs +0 -9
  97. package/templates/templates/spa/js/src/router/index.js.hbs +0 -41
  98. package/templates/templates/spa/js/src/vasp/auth.js.hbs +0 -45
  99. package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +0 -15
  100. package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +0 -30
  101. package/templates/templates/spa/js/src/vasp/client/index.js.hbs +0 -16
  102. package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +0 -15
  103. package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +0 -51
  104. package/templates/templates/spa/js/src/vasp/plugin.js.hbs +0 -11
  105. package/templates/templates/spa/js/vite.config.js.hbs +0 -26
  106. package/templates/templates/spa/ts/index.html.hbs +0 -12
  107. package/templates/templates/spa/ts/src/App.vue.hbs +0 -3
  108. package/templates/templates/spa/ts/src/main.ts.hbs +0 -9
  109. package/templates/templates/spa/ts/src/router/index.ts.hbs +0 -41
  110. package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +0 -53
  111. package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +0 -19
  112. package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +0 -37
  113. package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +0 -17
  114. package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +0 -19
  115. package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +0 -56
  116. package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +0 -33
  117. package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +0 -12
  118. package/templates/templates/spa/ts/vite.config.ts.hbs +0 -26
  119. package/templates/templates/ssr/js/_page.vue.hbs +0 -10
  120. package/templates/templates/ssr/js/app.vue.hbs +0 -3
  121. package/templates/templates/ssr/js/composables/useAuth.js.hbs +0 -52
  122. package/templates/templates/ssr/js/composables/useVasp.js.hbs +0 -6
  123. package/templates/templates/ssr/js/middleware/auth.js.hbs +0 -8
  124. package/templates/templates/ssr/js/nuxt.config.js.hbs +0 -15
  125. package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +0 -27
  126. package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +0 -33
  127. package/templates/templates/ssr/ts/_page.vue.hbs +0 -10
  128. package/templates/templates/ssr/ts/app.vue.hbs +0 -3
  129. package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +0 -56
  130. package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +0 -10
  131. package/templates/templates/ssr/ts/middleware/auth.ts.hbs +0 -8
  132. package/templates/templates/ssr/ts/nuxt.config.ts.hbs +0 -19
  133. package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +0 -27
  134. package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +0 -33
  135. /package/templates/{templates/shared → shared}/.env.hbs +0 -0
  136. /package/templates/{templates/shared → shared}/README.md.hbs +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasp-cli",
3
- "version": "0.4.0",
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
@@ -10,23 +10,13 @@ auth RecipeAuth {
10
10
  methods: [usernameAndPassword]
11
11
  }
12
12
 
13
- entity User {
14
- id: Int @id
15
- username: String @unique
16
- password: String
17
- createdAt: DateTime @default(now)
18
- }
19
-
20
13
  entity Recipe {
21
14
  id: Int @id
22
15
  title: String
23
16
  description: String
24
17
  ingredients: String
25
18
  instructions: String
26
- cookTime: Int
27
- servings: Int
28
- imageUrl: String
29
- createdAt: DateTime @default(now)
19
+ author: String
30
20
  }
31
21
 
32
22
  route HomeRoute {
@@ -40,30 +30,26 @@ route RecipesRoute {
40
30
  }
41
31
 
42
32
  route AddRecipeRoute {
43
- path: "/recipes/add"
33
+ path: "/recipes/new"
44
34
  to: AddRecipePage
45
35
  }
46
36
 
47
37
  page HomePage {
48
- component: import Home from "@src/pages/Home.vue"
38
+ component: import HomePage from "@src/pages/HomePage.vue"
49
39
  }
50
40
 
51
41
  page RecipesPage {
52
- component: import Recipes from "@src/pages/Recipes.vue"
42
+ component: import RecipesPage from "@src/pages/RecipesPage.vue"
53
43
  }
54
44
 
55
45
  page AddRecipePage {
56
- component: import AddRecipe from "@src/pages/AddRecipe.vue"
57
- }
58
-
59
- crud Recipe {
60
- entity: Recipe
61
- operations: [list, create, update, delete]
46
+ component: import AddRecipePage from "@src/pages/AddRecipePage.vue"
62
47
  }
63
48
 
64
49
  query getRecipes {
65
50
  fn: import { getRecipes } from "@src/queries.js"
66
51
  entities: [Recipe]
52
+ auth: true
67
53
  }
68
54
 
69
55
  action createRecipe {
@@ -77,3 +63,8 @@ action deleteRecipe {
77
63
  entities: [Recipe]
78
64
  auth: true
79
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
@@ -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
+ }
@@ -1,9 +1,7 @@
1
1
  import { Elysia } from 'elysia'
2
2
  import { jwt } from '@elysiajs/jwt'
3
- import { cookie } from '@elysiajs/cookie'
4
3
 
5
4
  const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'
6
5
 
7
6
  export const authPlugin = new Elysia({ name: 'auth-plugin' })
8
7
  .use(jwt({ name: 'jwt', secret: JWT_SECRET }))
9
- .use(cookie())
@@ -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, integer, boolean, timestamp, doublePrecision } 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,28 +29,39 @@ export type NewUser = InferInsertModel<typeof users>
21
29
  {{/if}}
22
30
 
23
31
  {{/if}}
24
- {{#each crudsWithFields}}
25
- {{#if hasEntity}}
26
- // {{entity}} table generated from entity block
27
- export const {{camelCase entity}}s = pgTable('{{camelCase entity}}s', {
28
- {{#each fields}}
29
- {{camelCase name}}: {{{drizzleColumn name type modifiers}}},
30
- {{/each}}
31
- createdAt: timestamp('created_at').defaultNow().notNull(),
32
- updatedAt: timestamp('updated_at').defaultNow().notNull(),
33
- })
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}}' }),
34
38
  {{else}}
35
- // {{entity}} table — no entity block found, add your columns below
36
- export const {{camelCase entity}}s = pgTable('{{camelCase entity}}s', {
37
- id: serial('id').primaryKey(),
38
- // TODO: Add your {{entity}} columns here
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}}
39
46
  createdAt: timestamp('created_at').defaultNow().notNull(),
40
47
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
41
48
  })
42
- {{/if}}
43
49
  {{#if ../isTypeScript}}
44
- export type {{pascalCase entity}} = InferSelectModel<typeof {{camelCase entity}}s>
45
- 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>
46
52
  {{/if}}
47
53
 
48
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}}
@@ -6,17 +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
11
  "dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}",
11
12
  "db:generate": "bunx drizzle-kit generate",
12
13
  "db:migrate": "bunx drizzle-kit migrate",
13
- "db:studio": "bunx drizzle-kit studio"
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}}
14
17
  },
15
18
  "dependencies": {
16
- "@vasp-framework/runtime": "^0.1.0",
19
+ "@vasp-framework/runtime": "^0.4.0",
17
20
  "elysia": "^1.1.0",
18
21
  "@elysiajs/cors": "^1.1.0",
19
- "@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",
20
26
  "drizzle-orm": "^0.36.0",
21
27
  "postgres": "^3.4.0",
22
28
  "ofetch": "^1.3.4",
@@ -26,7 +32,8 @@
26
32
  "pg-boss": "^10.0.0"{{/if}}
27
33
  },
28
34
  "devDependencies": {
29
- "drizzle-kit": "^0.28.0"{{#if isSpa}},
35
+ "drizzle-kit": "^0.28.0",
36
+ "vitest": "^2.1.9"{{#if isSpa}},
30
37
  "@vitejs/plugin-vue": "^5.2.0",
31
38
  "vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
32
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
+ })
@@ -2,19 +2,34 @@ import { Elysia } from 'elysia'
2
2
  import { cors } from '@elysiajs/cors'
3
3
  import { staticPlugin } from '@elysiajs/static'
4
4
  import { db } from './db/client.{{ext}}'
5
+ import { logger } from './middleware/logger.{{ext}}'
5
6
  import { rateLimit } from './middleware/rateLimit.{{ext}}'
7
+ import { errorHandler } from './middleware/errorHandler.{{ext}}'
8
+ import { vaspDiagnosticRoutes } from './routes/_vasp.{{ext}}'
6
9
  {{#if isSsr}}
7
10
  import { csrfProtection } from './middleware/csrf.{{ext}}'
8
11
  {{/if}}
9
12
  {{#if hasAuth}}
10
13
  import { authRoutes } from './auth/index.{{ext}}'
11
14
  {{/if}}
15
+ {{#each middlewares}}
16
+ {{#if (eq scope "global")}}
17
+ {{#if (eq fn.kind "default")}}
18
+ import {{importAlias}} from '{{importPath fnSource ../ext}}'
19
+ {{else}}
20
+ import { {{importName fn}} as {{importAlias}} } from '{{importPath fnSource ../ext}}'
21
+ {{/if}}
22
+ {{/if}}
23
+ {{/each}}
12
24
  {{#each queries}}
13
25
  import { {{camelCase name}}Route } from './routes/queries/{{camelCase name}}.{{../ext}}'
14
26
  {{/each}}
15
27
  {{#each actions}}
16
28
  import { {{camelCase name}}Route } from './routes/actions/{{camelCase name}}.{{../ext}}'
17
29
  {{/each}}
30
+ {{#each apis}}
31
+ import { {{camelCase name}}ApiRoute } from './routes/api/{{camelCase name}}.{{../ext}}'
32
+ {{/each}}
18
33
  {{#each cruds}}
19
34
  import { {{camelCase entity}}CrudRoutes } from './routes/crud/{{camelCase entity}}.{{../ext}}'
20
35
  {{/each}}
@@ -27,7 +42,23 @@ import { {{camelCase name}}ScheduleRoute } from './routes/jobs/{{camelCase name}
27
42
 
28
43
  const PORT = Number(process.env.PORT) || {{backendPort}}
29
44
 
45
+ const REQUIRED_ENV_VARS = [{{#each requiredEnvVars}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
46
+ const missingEnvVars = REQUIRED_ENV_VARS.filter((name) => {
47
+ const value = process.env[name]
48
+ return typeof value !== 'string' || value.trim() === ''
49
+ })
50
+
51
+ if (missingEnvVars.length > 0) {
52
+ console.error('❌ Missing required environment variables:')
53
+ for (const key of missingEnvVars) {
54
+ console.error(` - ${key}`)
55
+ }
56
+ process.exit(1)
57
+ }
58
+
30
59
  const app = new Elysia()
60
+ .use(logger())
61
+ .use(errorHandler())
31
62
  .use(cors({
32
63
  origin: process.env.CORS_ORIGIN || 'http://localhost:{{frontendPort}}',
33
64
  credentials: true,
@@ -37,15 +68,24 @@ const app = new Elysia()
37
68
  .use(csrfProtection())
38
69
  {{/if}}
39
70
  .get('/api/health', () => ({ status: 'ok', version: '{{vaspVersion}}' }))
71
+ .use(vaspDiagnosticRoutes)
40
72
  {{#if hasAuth}}
41
73
  .use(authRoutes)
42
74
  {{/if}}
75
+ {{#each middlewares}}
76
+ {{#if (eq scope "global")}}
77
+ .use({{importAlias}})
78
+ {{/if}}
79
+ {{/each}}
43
80
  {{#each queries}}
44
81
  .use({{camelCase name}}Route)
45
82
  {{/each}}
46
83
  {{#each actions}}
47
84
  .use({{camelCase name}}Route)
48
85
  {{/each}}
86
+ {{#each apis}}
87
+ .use({{camelCase name}}ApiRoute)
88
+ {{/each}}
49
89
  {{#each cruds}}
50
90
  .use({{camelCase entity}}CrudRoutes)
51
91
  {{/each}}