vasp-cli 0.4.0 → 1.0.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 (137) hide show
  1. package/README.md +57 -2
  2. package/dist/vasp +201 -35
  3. package/package.json +2 -2
  4. package/starters/minimal.vasp +1 -1
  5. package/starters/recipe.vasp +11 -20
  6. package/starters/todo-auth-ssr.vasp +33 -20
  7. package/starters/todo.vasp +15 -8
  8. package/templates/shared/.gitignore.hbs +1 -0
  9. package/templates/shared/auth/server/index.hbs +4 -8
  10. package/templates/shared/auth/server/middleware.hbs +33 -15
  11. package/templates/{templates/shared → shared}/auth/server/plugin.hbs +0 -2
  12. package/templates/shared/auth/server/providers/github.hbs +1 -1
  13. package/templates/shared/auth/server/providers/google.hbs +1 -1
  14. package/templates/shared/auth/server/providers/usernameAndPassword.hbs +3 -6
  15. package/templates/shared/bunfig.toml.hbs +3 -0
  16. package/templates/shared/drizzle/schema.hbs +38 -19
  17. package/templates/shared/package.json.hbs +11 -3
  18. package/templates/shared/server/db/client.hbs +19 -1
  19. package/templates/shared/server/db/seed.hbs +16 -0
  20. package/templates/shared/server/index.hbs +47 -0
  21. package/templates/shared/server/middleware/errorHandler.hbs +75 -0
  22. package/templates/shared/server/middleware/logger.hbs +74 -0
  23. package/templates/shared/server/middleware/rateLimit.hbs +2 -2
  24. package/templates/shared/server/routes/_vasp.hbs +37 -0
  25. package/templates/shared/server/routes/actions/_action.hbs +5 -1
  26. package/templates/shared/server/routes/api/_api.hbs +24 -0
  27. package/templates/shared/server/routes/crud/_crud.hbs +58 -10
  28. package/templates/shared/server/routes/queries/_query.hbs +5 -1
  29. package/templates/shared/shared/types.hbs +58 -0
  30. package/templates/shared/shared/validation.hbs +20 -0
  31. package/templates/shared/tests/actions/_action.test.js.hbs +7 -0
  32. package/templates/shared/tests/actions/_action.test.ts.hbs +7 -0
  33. package/templates/shared/tests/auth/login.test.js.hbs +7 -0
  34. package/templates/shared/tests/auth/login.test.ts.hbs +7 -0
  35. package/templates/shared/tests/crud/_entity.test.js.hbs +7 -0
  36. package/templates/shared/tests/crud/_entity.test.ts.hbs +7 -0
  37. package/templates/shared/tests/queries/_query.test.js.hbs +7 -0
  38. package/templates/shared/tests/queries/_query.test.ts.hbs +7 -0
  39. package/templates/shared/tests/setup.js.hbs +5 -0
  40. package/templates/shared/tests/setup.ts.hbs +5 -0
  41. package/templates/shared/tests/vitest.config.js.hbs +8 -0
  42. package/templates/shared/tests/vitest.config.ts.hbs +8 -0
  43. package/templates/shared/tsconfig.json.hbs +2 -1
  44. package/templates/spa/js/src/App.vue.hbs +9 -1
  45. package/templates/spa/js/src/components/VaspErrorBoundary.vue.hbs +33 -0
  46. package/templates/spa/js/src/components/VaspNotifications.vue.hbs +60 -0
  47. package/templates/spa/js/src/vasp/auth.js.hbs +31 -15
  48. package/templates/spa/js/src/vasp/client/actions.js.hbs +7 -1
  49. package/templates/spa/js/src/vasp/client/crud.js.hbs +94 -5
  50. package/templates/spa/js/src/vasp/useVaspNotifications.js.hbs +35 -0
  51. package/templates/spa/js/vite.config.js.hbs +1 -0
  52. package/templates/spa/ts/src/App.vue.hbs +9 -1
  53. package/templates/spa/ts/src/components/VaspErrorBoundary.vue.hbs +33 -0
  54. package/templates/spa/ts/src/components/VaspNotifications.vue.hbs +60 -0
  55. package/templates/spa/ts/src/vasp/auth.ts.hbs +31 -15
  56. package/templates/spa/ts/src/vasp/client/actions.ts.hbs +7 -1
  57. package/templates/spa/ts/src/vasp/client/crud.ts.hbs +96 -10
  58. package/templates/spa/ts/src/vasp/client/types.ts.hbs +14 -28
  59. package/templates/spa/ts/src/vasp/useVaspNotifications.ts.hbs +41 -0
  60. package/templates/spa/ts/vite.config.ts.hbs +1 -0
  61. package/templates/ssr/js/error.vue.hbs +23 -0
  62. package/templates/ssr/js/nuxt.config.js.hbs +1 -0
  63. package/templates/ssr/ts/error.vue.hbs +26 -0
  64. package/templates/ssr/ts/nuxt.config.ts.hbs +1 -0
  65. package/templates/starters/minimal.vasp +15 -0
  66. package/templates/starters/recipe.vasp +70 -0
  67. package/templates/starters/todo-auth-ssr.vasp +65 -0
  68. package/templates/starters/todo.vasp +42 -0
  69. package/templates/templates/shared/.env.example.hbs +0 -14
  70. package/templates/templates/shared/.gitignore.hbs +0 -8
  71. package/templates/templates/shared/auth/client/Login.vue.hbs +0 -46
  72. package/templates/templates/shared/auth/client/Register.vue.hbs +0 -42
  73. package/templates/templates/shared/auth/server/index.hbs +0 -46
  74. package/templates/templates/shared/auth/server/middleware.hbs +0 -33
  75. package/templates/templates/shared/auth/server/providers/github.hbs +0 -48
  76. package/templates/templates/shared/auth/server/providers/google.hbs +0 -53
  77. package/templates/templates/shared/auth/server/providers/usernameAndPassword.hbs +0 -66
  78. package/templates/templates/shared/bunfig.toml.hbs +0 -5
  79. package/templates/templates/shared/drizzle/drizzle.config.hbs +0 -10
  80. package/templates/templates/shared/drizzle/schema.hbs +0 -48
  81. package/templates/templates/shared/jobs/_job.hbs +0 -34
  82. package/templates/templates/shared/jobs/boss.hbs +0 -15
  83. package/templates/templates/shared/package.json.hbs +0 -38
  84. package/templates/templates/shared/server/db/client.hbs +0 -12
  85. package/templates/templates/shared/server/index.hbs +0 -73
  86. package/templates/templates/shared/server/middleware/csrf.hbs +0 -34
  87. package/templates/templates/shared/server/middleware/rateLimit.hbs +0 -44
  88. package/templates/templates/shared/server/routes/actions/_action.hbs +0 -20
  89. package/templates/templates/shared/server/routes/crud/_crud.hbs +0 -86
  90. package/templates/templates/shared/server/routes/jobs/_schedule.hbs +0 -12
  91. package/templates/templates/shared/server/routes/queries/_query.hbs +0 -20
  92. package/templates/templates/shared/server/routes/realtime/_channel.hbs +0 -78
  93. package/templates/templates/shared/server/routes/realtime/index.hbs +0 -9
  94. package/templates/templates/shared/tsconfig.json.hbs +0 -21
  95. package/templates/templates/spa/js/index.html.hbs +0 -12
  96. package/templates/templates/spa/js/src/App.vue.hbs +0 -3
  97. package/templates/templates/spa/js/src/main.js.hbs +0 -9
  98. package/templates/templates/spa/js/src/router/index.js.hbs +0 -41
  99. package/templates/templates/spa/js/src/vasp/auth.js.hbs +0 -45
  100. package/templates/templates/spa/js/src/vasp/client/actions.js.hbs +0 -15
  101. package/templates/templates/spa/js/src/vasp/client/crud.js.hbs +0 -30
  102. package/templates/templates/spa/js/src/vasp/client/index.js.hbs +0 -16
  103. package/templates/templates/spa/js/src/vasp/client/queries.js.hbs +0 -15
  104. package/templates/templates/spa/js/src/vasp/client/realtime.js.hbs +0 -51
  105. package/templates/templates/spa/js/src/vasp/plugin.js.hbs +0 -11
  106. package/templates/templates/spa/js/vite.config.js.hbs +0 -26
  107. package/templates/templates/spa/ts/index.html.hbs +0 -12
  108. package/templates/templates/spa/ts/src/App.vue.hbs +0 -3
  109. package/templates/templates/spa/ts/src/main.ts.hbs +0 -9
  110. package/templates/templates/spa/ts/src/router/index.ts.hbs +0 -41
  111. package/templates/templates/spa/ts/src/vasp/auth.ts.hbs +0 -53
  112. package/templates/templates/spa/ts/src/vasp/client/actions.ts.hbs +0 -19
  113. package/templates/templates/spa/ts/src/vasp/client/crud.ts.hbs +0 -37
  114. package/templates/templates/spa/ts/src/vasp/client/index.ts.hbs +0 -17
  115. package/templates/templates/spa/ts/src/vasp/client/queries.ts.hbs +0 -19
  116. package/templates/templates/spa/ts/src/vasp/client/realtime.ts.hbs +0 -56
  117. package/templates/templates/spa/ts/src/vasp/client/types.ts.hbs +0 -33
  118. package/templates/templates/spa/ts/src/vasp/plugin.ts.hbs +0 -12
  119. package/templates/templates/spa/ts/vite.config.ts.hbs +0 -26
  120. package/templates/templates/ssr/js/_page.vue.hbs +0 -10
  121. package/templates/templates/ssr/js/app.vue.hbs +0 -3
  122. package/templates/templates/ssr/js/composables/useAuth.js.hbs +0 -52
  123. package/templates/templates/ssr/js/composables/useVasp.js.hbs +0 -6
  124. package/templates/templates/ssr/js/middleware/auth.js.hbs +0 -8
  125. package/templates/templates/ssr/js/nuxt.config.js.hbs +0 -15
  126. package/templates/templates/ssr/js/plugins/vasp.client.js.hbs +0 -27
  127. package/templates/templates/ssr/js/plugins/vasp.server.js.hbs +0 -33
  128. package/templates/templates/ssr/ts/_page.vue.hbs +0 -10
  129. package/templates/templates/ssr/ts/app.vue.hbs +0 -3
  130. package/templates/templates/ssr/ts/composables/useAuth.ts.hbs +0 -56
  131. package/templates/templates/ssr/ts/composables/useVasp.ts.hbs +0 -10
  132. package/templates/templates/ssr/ts/middleware/auth.ts.hbs +0 -8
  133. package/templates/templates/ssr/ts/nuxt.config.ts.hbs +0 -19
  134. package/templates/templates/ssr/ts/plugins/vasp.client.ts.hbs +0 -27
  135. package/templates/templates/ssr/ts/plugins/vasp.server.ts.hbs +0 -33
  136. /package/templates/{templates/shared → shared}/.env.hbs +0 -0
  137. /package/templates/{templates/shared → shared}/README.md.hbs +0 -0
@@ -1,5 +0,0 @@
1
- [install]
2
- exact = false
3
-
4
- [resolve]
5
- "@src" = "./src"
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'drizzle-kit'
2
-
3
- export default defineConfig({
4
- schema: './drizzle/schema.{{ext}}',
5
- out: './drizzle/migrations',
6
- dialect: 'postgresql',
7
- dbCredentials: {
8
- url: process.env.DATABASE_URL,
9
- },
10
- })
@@ -1,48 +0,0 @@
1
- import { pgTable, serial, text, integer, boolean, timestamp, doublePrecision } from 'drizzle-orm/pg-core'
2
- {{#if isTypeScript}}
3
- import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'
4
- {{/if}}
5
-
6
- {{#if hasAuth}}
7
- // Users table — generated by Vasp auth system
8
- export const users = pgTable('users', {
9
- id: serial('id').primaryKey(),
10
- username: text('username').notNull().unique(),
11
- email: text('email').unique(),
12
- passwordHash: text('password_hash'),
13
- googleId: text('google_id').unique(),
14
- githubId: text('github_id').unique(),
15
- createdAt: timestamp('created_at').defaultNow().notNull(),
16
- updatedAt: timestamp('updated_at').defaultNow().notNull(),
17
- })
18
- {{#if isTypeScript}}
19
- export type User = InferSelectModel<typeof users>
20
- export type NewUser = InferInsertModel<typeof users>
21
- {{/if}}
22
-
23
- {{/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
- })
34
- {{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
- createdAt: timestamp('created_at').defaultNow().notNull(),
40
- updatedAt: timestamp('updated_at').defaultNow().notNull(),
41
- })
42
- {{/if}}
43
- {{#if ../isTypeScript}}
44
- export type {{pascalCase entity}} = InferSelectModel<typeof {{camelCase entity}}s>
45
- export type New{{pascalCase entity}} = InferInsertModel<typeof {{camelCase entity}}s>
46
- {{/if}}
47
-
48
- {{/each}}
@@ -1,34 +0,0 @@
1
- import { getBoss } from './boss.{{ext}}'
2
- import { {{namedExport}} } from '{{importPath fnSource ext}}'
3
-
4
- const JOB_NAME = '{{camelCase name}}'
5
-
6
- /**
7
- * Register the '{{name}}' job worker with PgBoss.
8
- * Called once on server startup.
9
- */
10
- export async function register{{pascalCase name}}Worker() {
11
- const boss = await getBoss()
12
- await boss.work(JOB_NAME, async (job) => {
13
- await {{namedExport}}(job.data)
14
- })
15
- {{#if hasSchedule}}
16
-
17
- // Register cron schedule: {{schedule}}
18
- await boss.schedule(JOB_NAME, '{{schedule}}', {})
19
- {{/if}}
20
- }
21
-
22
- /**
23
- * Schedule a '{{name}}' job on demand.
24
- * @param {unknown} data - Data to pass to the job handler
25
- */
26
- export async function schedule{{pascalCase name}}(data) {
27
- const boss = await getBoss()
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
- })
34
- }
@@ -1,15 +0,0 @@
1
- import PgBoss from 'pg-boss'
2
-
3
- const connectionString = process.env.DATABASE_URL
4
- if (!connectionString) throw new Error('DATABASE_URL is required for PgBoss job queue')
5
-
6
- // Singleton PgBoss instance shared across all job workers
7
- let boss = null
8
-
9
- export async function getBoss() {
10
- if (!boss) {
11
- boss = new PgBoss(connectionString)
12
- await boss.start()
13
- }
14
- return boss
15
- }
@@ -1,38 +0,0 @@
1
- {
2
- "name": "{{kebabCase appName}}",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vasp start",
8
- "build": "vasp build",
9
- "dev:server": "bun --hot server/index.{{ext}}",
10
- "dev:client": "{{#if isSpa}}vite{{else}}nuxt dev{{/if}}",
11
- "db:generate": "bunx drizzle-kit generate",
12
- "db:migrate": "bunx drizzle-kit migrate",
13
- "db:push": "bunx drizzle-kit push",
14
- "db:studio": "bunx drizzle-kit studio"
15
- },
16
- "dependencies": {
17
- "@vasp-framework/runtime": "^0.1.0",
18
- "elysia": "^1.1.0",
19
- "@elysiajs/cors": "^1.1.0",
20
- "@elysiajs/static": "^1.1.0",{{#if auth}}
21
- "@elysiajs/jwt": "^1.1.0",
22
- "@elysiajs/cookie": "^0.8.0",{{/if}}
23
- "drizzle-orm": "^0.36.0",
24
- "postgres": "^3.4.0",
25
- "ofetch": "^1.3.4",
26
- "vue": "^3.5.0"{{#if isSpa}},
27
- "vue-router": "^4.4.0"{{else}},
28
- "nuxt": "^4.0.0"{{/if}}{{#if hasJobs}},
29
- "pg-boss": "^10.0.0"{{/if}}
30
- },
31
- "devDependencies": {
32
- "drizzle-kit": "^0.28.0"{{#if isSpa}},
33
- "@vitejs/plugin-vue": "^5.2.0",
34
- "vite": "^6.0.0"{{/if}}{{#if isTypeScript}},
35
- "typescript": "^5.6.0",
36
- "vue-tsc": "^2.0.0"{{/if}}
37
- }
38
- }
@@ -1,12 +0,0 @@
1
- import { drizzle } from 'drizzle-orm/postgres-js'
2
- import postgres from 'postgres'
3
- import * as schema from '../../drizzle/schema.{{ext}}'
4
-
5
- const connectionString = process.env.DATABASE_URL
6
-
7
- if (!connectionString) {
8
- throw new Error('DATABASE_URL environment variable is required')
9
- }
10
-
11
- const client = postgres(connectionString)
12
- export const db = drizzle(client, { schema })
@@ -1,73 +0,0 @@
1
- import { Elysia } from 'elysia'
2
- import { cors } from '@elysiajs/cors'
3
- import { staticPlugin } from '@elysiajs/static'
4
- import { db } from './db/client.{{ext}}'
5
- import { rateLimit } from './middleware/rateLimit.{{ext}}'
6
- {{#if isSsr}}
7
- import { csrfProtection } from './middleware/csrf.{{ext}}'
8
- {{/if}}
9
- {{#if hasAuth}}
10
- import { authRoutes } from './auth/index.{{ext}}'
11
- {{/if}}
12
- {{#each queries}}
13
- import { {{camelCase name}}Route } from './routes/queries/{{camelCase name}}.{{../ext}}'
14
- {{/each}}
15
- {{#each actions}}
16
- import { {{camelCase name}}Route } from './routes/actions/{{camelCase name}}.{{../ext}}'
17
- {{/each}}
18
- {{#each cruds}}
19
- import { {{camelCase entity}}CrudRoutes } from './routes/crud/{{camelCase entity}}.{{../ext}}'
20
- {{/each}}
21
- {{#if hasRealtime}}
22
- import { realtimeRoutes } from './routes/realtime/index.{{ext}}'
23
- {{/if}}
24
- {{#each jobs}}
25
- import { {{camelCase name}}ScheduleRoute } from './routes/jobs/{{camelCase name}}Schedule.{{../ext}}'
26
- {{/each}}
27
-
28
- const PORT = Number(process.env.PORT) || {{backendPort}}
29
-
30
- const app = new Elysia()
31
- .use(cors({
32
- origin: process.env.CORS_ORIGIN || 'http://localhost:{{frontendPort}}',
33
- credentials: true,
34
- }))
35
- .use(rateLimit())
36
- {{#if isSsr}}
37
- .use(csrfProtection())
38
- {{/if}}
39
- .get('/api/health', () => ({ status: 'ok', version: '{{vaspVersion}}' }))
40
- {{#if hasAuth}}
41
- .use(authRoutes)
42
- {{/if}}
43
- {{#each queries}}
44
- .use({{camelCase name}}Route)
45
- {{/each}}
46
- {{#each actions}}
47
- .use({{camelCase name}}Route)
48
- {{/each}}
49
- {{#each cruds}}
50
- .use({{camelCase entity}}CrudRoutes)
51
- {{/each}}
52
- {{#if hasRealtime}}
53
- .use(realtimeRoutes)
54
- {{/if}}
55
- {{#each jobs}}
56
- .use({{camelCase name}}ScheduleRoute)
57
- {{/each}}
58
- .onError(({ code, error, set }) => {
59
- if (code === 'NOT_FOUND') {
60
- set.status = 404
61
- return { error: 'Not found' }
62
- }
63
- if (code === 'VALIDATION') {
64
- set.status = 400
65
- return { error: 'Validation failed', details: error.message }
66
- }
67
- console.error('[server error]', error)
68
- set.status = 500
69
- return { error: 'Internal server error' }
70
- })
71
- .listen(PORT)
72
-
73
- console.log(`🚀 Vasp backend running at http://localhost:${PORT}`)
@@ -1,34 +0,0 @@
1
- import { Elysia } from 'elysia'
2
- import { randomBytes } from 'node:crypto'
3
-
4
- const CSRF_COOKIE = 'vasp-csrf'
5
- const CSRF_HEADER = 'x-csrf-token'
6
- const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
7
-
8
- /**
9
- * CSRF protection middleware — double-submit cookie pattern.
10
- * On every response, sets a CSRF cookie with a random token.
11
- * On state-changing requests (POST, PUT, DELETE), validates that the
12
- * x-csrf-token header matches the cookie value.
13
- */
14
- export function csrfProtection() {
15
- return new Elysia({ name: 'csrf' })
16
- .onRequest(({ request, set, cookie }) => {
17
- // Generate token if not already set
18
- if (!cookie[CSRF_COOKIE]?.value) {
19
- const token = randomBytes(32).toString('hex')
20
- cookie[CSRF_COOKIE].set({ value: token, httpOnly: false, sameSite: 'strict', path: '/' })
21
- }
22
-
23
- // Skip validation for safe methods
24
- if (SAFE_METHODS.has(request.method)) return
25
-
26
- const cookieToken = cookie[CSRF_COOKIE]?.value
27
- const headerToken = request.headers.get(CSRF_HEADER)
28
-
29
- if (!cookieToken || !headerToken || cookieToken !== headerToken) {
30
- set.status = 403
31
- return { error: 'CSRF token mismatch' }
32
- }
33
- })
34
- }
@@ -1,44 +0,0 @@
1
- import { Elysia } from 'elysia'
2
-
3
- const MAX_REQUESTS = Number(process.env.RATE_LIMIT_MAX) || 100
4
- const WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 60_000
5
-
6
- const hits = new Map()
7
-
8
- // Clean up expired entries every 60 seconds
9
- setInterval(() => {
10
- const now = Date.now()
11
- for (const [key, entry] of hits) {
12
- if (now - entry.start > WINDOW_MS) hits.delete(key)
13
- }
14
- }, 60_000)
15
-
16
- /**
17
- * Simple in-memory sliding-window rate limiter.
18
- * Limits each IP to MAX_REQUESTS per WINDOW_MS.
19
- */
20
- export function rateLimit() {
21
- return new Elysia({ name: 'rate-limit' })
22
- .onBeforeHandle(({ request, set }) => {
23
- const ip = request.headers.get('x-forwarded-for')
24
- || request.headers.get('x-real-ip')
25
- || 'unknown'
26
- const now = Date.now()
27
- let entry = hits.get(ip)
28
-
29
- if (!entry || now - entry.start > WINDOW_MS) {
30
- entry = { start: now, count: 0 }
31
- hits.set(ip, entry)
32
- }
33
-
34
- entry.count++
35
-
36
- set.headers['x-ratelimit-limit'] = String(MAX_REQUESTS)
37
- set.headers['x-ratelimit-remaining'] = String(Math.max(0, MAX_REQUESTS - entry.count))
38
-
39
- if (entry.count > MAX_REQUESTS) {
40
- set.status = 429
41
- return { error: 'Too many requests. Please try again later.' }
42
- }
43
- })
44
- }
@@ -1,20 +0,0 @@
1
- import { Elysia } from 'elysia'
2
- import { db } from '../../db/client.{{ext}}'
3
- {{#if requiresAuth}}
4
- import { requireAuth } from '../../auth/middleware.{{ext}}'
5
- {{/if}}
6
- import { {{namedExport}} } from '{{importPath fnSource ext}}'
7
-
8
- export const {{camelCase name}}Route = new Elysia()
9
- {{#if requiresAuth}}
10
- .use(requireAuth)
11
- .post('/api/actions/{{camelCase name}}', async ({ body, user }) => {
12
- const result = await {{namedExport}}({ db, user, args: body })
13
- return result
14
- })
15
- {{else}}
16
- .post('/api/actions/{{camelCase name}}', async ({ body }) => {
17
- const result = await {{namedExport}}({ db, args: body })
18
- return result
19
- })
20
- {{/if}}
@@ -1,86 +0,0 @@
1
- import { Elysia, t } from 'elysia'
2
- import { db } from '../../db/client.{{ext}}'
3
- import { eq, sql, asc, desc, and, ilike } from 'drizzle-orm'
4
- import { {{camelCase entity}}s } from '../../../drizzle/schema.{{ext}}'
5
- {{#if hasAuth}}
6
- import { requireAuth } from '../../auth/middleware.{{ext}}'
7
- {{/if}}
8
- {{#if hasRealtime}}
9
- import { publish{{pascalCase entity}} } from '../realtime/{{camelCase realtimeName}}.{{ext}}'
10
- {{/if}}
11
-
12
- export const {{camelCase entity}}CrudRoutes = new Elysia({ prefix: '/api/crud/{{camelCase entity}}' })
13
- {{#if hasAuth}}
14
- .use(requireAuth)
15
- {{/if}}
16
- {{#if (includes operations "list")}}
17
- .get('/', async ({ query }) => {
18
- const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100)
19
- const offset = Math.max(Number(query.offset) || 0, 0)
20
-
21
- const table = {{camelCase entity}}s
22
-
23
- // Multi-column sorting: orderBy=col1,col2 & dir=asc,desc
24
- const orderByFields = (query.orderBy ?? 'id').split(',')
25
- const directions = (query.dir ?? 'asc').split(',')
26
- const orderClauses = orderByFields.map((field, i) => {
27
- const col = table[field.trim()] ?? table.id
28
- const dirFn = (directions[i] ?? directions[0] ?? 'asc').trim() === 'desc' ? desc : asc
29
- return dirFn(col)
30
- })
31
-
32
- // Build WHERE conditions from filter.* query params
33
- const conditions = []
34
- for (const [key, value] of Object.entries(query)) {
35
- if (!key.startsWith('filter.')) continue
36
- const field = key.slice(7)
37
- if (!table[field]) continue
38
- conditions.push(eq(table[field], value))
39
- }
40
- const where = conditions.length > 0 ? and(...conditions) : undefined
41
-
42
- const baseQuery = db.select().from(table)
43
- const countQuery = db.select({ count: sql`count(*)::int` }).from(table)
44
-
45
- const [data, countResult] = await Promise.all([
46
- (where ? baseQuery.where(where) : baseQuery).orderBy(...orderClauses).limit(limit).offset(offset),
47
- where ? countQuery.where(where) : countQuery,
48
- ])
49
-
50
- return { data, total: countResult[0]?.count ?? 0, limit, offset }
51
- })
52
- {{/if}}
53
- {{#if (includes operations "create")}}
54
- .post('/', async ({ body }) => {
55
- const [created] = await db.insert({{camelCase entity}}s).values(body).returning()
56
- {{#if hasRealtime}}
57
- publish{{pascalCase entity}}('created', created)
58
- {{/if}}
59
- return created
60
- })
61
- {{/if}}
62
- .get('/:id', async ({ params: { id }, set }) => {
63
- const [item] = await db.select().from({{camelCase entity}}s).where(eq({{camelCase entity}}s.id, Number(id))).limit(1)
64
- if (!item) { set.status = 404; return { error: 'Not found' } }
65
- return item
66
- })
67
- {{#if (includes operations "update")}}
68
- .put('/:id', async ({ params: { id }, body, set }) => {
69
- const [updated] = await db.update({{camelCase entity}}s).set(body).where(eq({{camelCase entity}}s.id, Number(id))).returning()
70
- if (!updated) { set.status = 404; return { error: 'Not found' } }
71
- {{#if hasRealtime}}
72
- publish{{pascalCase entity}}('updated', updated)
73
- {{/if}}
74
- return updated
75
- })
76
- {{/if}}
77
- {{#if (includes operations "delete")}}
78
- .delete('/:id', async ({ params: { id }, set }) => {
79
- const [deleted] = await db.delete({{camelCase entity}}s).where(eq({{camelCase entity}}s.id, Number(id))).returning()
80
- if (!deleted) { set.status = 404; return { error: 'Not found' } }
81
- {{#if hasRealtime}}
82
- publish{{pascalCase entity}}('deleted', deleted)
83
- {{/if}}
84
- return { ok: true }
85
- })
86
- {{/if}}
@@ -1,12 +0,0 @@
1
- import { Elysia, t } from 'elysia'
2
- import { schedule{{pascalCase name}} } from '../../../jobs/{{camelCase name}}.{{ext}}'
3
-
4
- export const {{camelCase name}}ScheduleRoute = new Elysia()
5
- .post(
6
- '/api/jobs/{{camelCase name}}/schedule',
7
- async ({ body }) => {
8
- const id = await schedule{{pascalCase name}}(body)
9
- return { jobId: id }
10
- },
11
- { body: t.Unknown() },
12
- )
@@ -1,20 +0,0 @@
1
- import { Elysia } from 'elysia'
2
- import { db } from '../../db/client.{{ext}}'
3
- {{#if requiresAuth}}
4
- import { requireAuth } from '../../auth/middleware.{{ext}}'
5
- {{/if}}
6
- import { {{namedExport}} } from '{{importPath fnSource ext}}'
7
-
8
- export const {{camelCase name}}Route = new Elysia()
9
- {{#if requiresAuth}}
10
- .use(requireAuth)
11
- .get('/api/queries/{{camelCase name}}', async ({ query, user }) => {
12
- const result = await {{namedExport}}({ db, user, args: query })
13
- return result
14
- })
15
- {{else}}
16
- .get('/api/queries/{{camelCase name}}', async ({ query }) => {
17
- const result = await {{namedExport}}({ db, args: query })
18
- return result
19
- })
20
- {{/if}}
@@ -1,78 +0,0 @@
1
- import { Elysia } from 'elysia'
2
- {{#if ../hasAuth}}
3
- import jwt from '@elysiajs/jwt'
4
- {{/if}}
5
-
6
- // Room-based subscriber map for '{{name}}' channel
7
- // Each room key maps to a set of connected WebSocket clients
8
- const rooms = new Map()
9
-
10
- function getRoom(roomId) {
11
- if (!rooms.has(roomId)) rooms.set(roomId, new Set())
12
- return rooms.get(roomId)
13
- }
14
-
15
- /**
16
- * Publish a realtime event to all subscribers in a specific room of '{{name}}'.
17
- * Called automatically by CRUD mutation handlers.
18
- */
19
- export function publish{{pascalCase name}}(event, data, roomId = 'default') {
20
- const room = rooms.get(roomId)
21
- if (!room) return
22
- const message = JSON.stringify({ channel: '{{camelCase name}}', room: roomId, event, data })
23
- for (const ws of room) {
24
- try { ws.send(message) } catch { room.delete(ws) }
25
- }
26
- }
27
-
28
- /**
29
- * Broadcast to all rooms of '{{name}}'.
30
- */
31
- export function broadcast{{pascalCase name}}(event, data) {
32
- const message = JSON.stringify({ channel: '{{camelCase name}}', event, data })
33
- for (const [, room] of rooms) {
34
- for (const ws of room) {
35
- try { ws.send(message) } catch { room.delete(ws) }
36
- }
37
- }
38
- }
39
-
40
- export const {{camelCase name}}Channel = new Elysia()
41
- {{#if ../hasAuth}}
42
- .use(jwt({ name: 'jwt', secret: process.env.JWT_SECRET || 'vasp-dev-secret' }))
43
- {{/if}}
44
- .ws('/ws/{{camelCase name}}', {
45
- {{#if ../hasAuth}}
46
- async beforeHandle({ jwt: jwtPlugin, request }) {
47
- const url = new URL(request.url)
48
- const token = url.searchParams.get('token')
49
- if (!token) return new Response('Unauthorized', { status: 401 })
50
- const payload = await jwtPlugin.verify(token)
51
- if (!payload) return new Response('Unauthorized', { status: 401 })
52
- },
53
- {{/if}}
54
- open(ws) {
55
- const room = ws.data?.query?.room ?? 'default'
56
- getRoom(room).add(ws)
57
- ws.data._room = room
58
- },
59
- message(ws, msg) {
60
- // Client can switch rooms via { action: 'join', room: 'roomId' }
61
- try {
62
- const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg
63
- if (parsed.action === 'join' && parsed.room) {
64
- const oldRoom = ws.data._room
65
- if (oldRoom) rooms.get(oldRoom)?.delete(ws)
66
- ws.data._room = parsed.room
67
- getRoom(parsed.room).add(ws)
68
- ws.send(JSON.stringify({ ack: 'joined', room: parsed.room }))
69
- return
70
- }
71
- } catch { /* ignore parse errors */ }
72
- ws.send(JSON.stringify({ ack: msg }))
73
- },
74
- close(ws) {
75
- const room = ws.data?._room
76
- if (room) rooms.get(room)?.delete(ws)
77
- },
78
- })
@@ -1,9 +0,0 @@
1
- import { Elysia } from 'elysia'
2
- {{#each realtimes}}
3
- import { {{camelCase name}}Channel } from './{{camelCase name}}.{{../ext}}'
4
- {{/each}}
5
-
6
- export const realtimeRoutes = new Elysia()
7
- {{#each realtimes}}
8
- .use({{camelCase name}}Channel)
9
- {{/each}}
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
7
- "strict": true,
8
- "noUnusedLocals": true,
9
- "noUnusedParameters": true,
10
- "noFallthroughCasesInSwitch": true,
11
- "esModuleInterop": true,
12
- "skipLibCheck": true,
13
- "jsx": "preserve",
14
- "paths": {
15
- "@src/*": ["./src/*"],
16
- "@vasp-framework/client": ["./src/vasp/client/index.ts"]
17
- }
18
- },
19
- "include": ["src/**/*.ts", "src/**/*.vue", "server/**/*.ts", "drizzle/**/*.ts"],
20
- "exclude": ["node_modules", "dist", ".vasp-gen"]
21
- }
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>{{appTitle}}</title>
7
- </head>
8
- <body>
9
- <div id="app"></div>
10
- <script type="module" src="/src/main.js"></script>
11
- </body>
12
- </html>
@@ -1,3 +0,0 @@
1
- <template>
2
- <RouterView />
3
- </template>
@@ -1,9 +0,0 @@
1
- import { createApp } from 'vue'
2
- import App from './App.vue'
3
- import router from './router/index.js'
4
- import { vaspPlugin } from './vasp/plugin.js'
5
-
6
- const app = createApp(App)
7
- app.use(router)
8
- app.use(vaspPlugin)
9
- app.mount('#app')
@@ -1,41 +0,0 @@
1
- import { createRouter, createWebHistory } from 'vue-router'
2
- {{#if hasAuth}}
3
- import { useAuth } from '../vasp/auth.js'
4
- {{/if}}
5
-
6
- const routes = [
7
- {{#each routes}}
8
- {
9
- path: '{{path}}',
10
- component: () => import('{{lookup ../pagesMap to}}'),
11
- },
12
- {{/each}}
13
- {{#if hasAuth}}
14
- {
15
- path: '/login',
16
- component: () => import('../pages/Login.vue'),
17
- },
18
- {
19
- path: '/register',
20
- component: () => import('../pages/Register.vue'),
21
- },
22
- {{/if}}
23
- ]
24
-
25
- const router = createRouter({
26
- history: createWebHistory(),
27
- routes,
28
- })
29
-
30
- {{#if hasAuth}}
31
- router.beforeEach(async (to) => {
32
- const { user, checkAuth } = useAuth()
33
- await checkAuth()
34
- const publicPaths = ['/login', '/register']
35
- if (!user.value && !publicPaths.includes(to.path)) {
36
- return '/login'
37
- }
38
- })
39
- {{/if}}
40
-
41
- export default router