@strav/spring 0.3.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 (40) hide show
  1. package/README.md +61 -0
  2. package/package.json +31 -0
  3. package/src/index.ts +176 -0
  4. package/src/prompts.ts +135 -0
  5. package/src/scaffold.ts +54 -0
  6. package/src/templates/api/app/controllers/user_controller.ts +69 -0
  7. package/src/templates/api/config/http.ts +10 -0
  8. package/src/templates/api/index.ts +33 -0
  9. package/src/templates/api/routes/routes.ts +24 -0
  10. package/src/templates/shared/.env +14 -0
  11. package/src/templates/shared/app/controllers/controller.ts +15 -0
  12. package/src/templates/shared/app/models/user.ts +30 -0
  13. package/src/templates/shared/config/app.ts +10 -0
  14. package/src/templates/shared/config/database.ts +9 -0
  15. package/src/templates/shared/config/encryption.ts +5 -0
  16. package/src/templates/shared/database/factories/user_factory.ts +11 -0
  17. package/src/templates/shared/database/schemas/public/user.ts +13 -0
  18. package/src/templates/shared/database/seeders/database_seeder.ts +8 -0
  19. package/src/templates/shared/database/seeders/user_seeder.ts +15 -0
  20. package/src/templates/shared/package.json +24 -0
  21. package/src/templates/shared/routes/routes.ts +13 -0
  22. package/src/templates/shared/storage/cache/.gitkeep +1 -0
  23. package/src/templates/shared/storage/logs/.gitkeep +1 -0
  24. package/src/templates/shared/storage/uploads/.gitkeep +1 -0
  25. package/src/templates/shared/strav.ts +20 -0
  26. package/src/templates/shared/tests/example.test.ts +11 -0
  27. package/src/templates/shared/tsconfig.json +20 -0
  28. package/src/templates/web/app/controllers/home_controller.ts +24 -0
  29. package/src/templates/web/config/session.ts +10 -0
  30. package/src/templates/web/config/view.ts +7 -0
  31. package/src/templates/web/index.ts +48 -0
  32. package/src/templates/web/package.json +26 -0
  33. package/src/templates/web/resources/css/app.css +176 -0
  34. package/src/templates/web/resources/ts/islands/counter.vue +42 -0
  35. package/src/templates/web/resources/ts/islands/user_manager.vue +127 -0
  36. package/src/templates/web/resources/ts/islands/user_search.vue +71 -0
  37. package/src/templates/web/resources/views/layouts/app.strav +32 -0
  38. package/src/templates/web/resources/views/pages/home.strav +52 -0
  39. package/src/templates/web/resources/views/pages/users.strav +63 -0
  40. package/src/templates/web/routes/routes.ts +22 -0
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @strav/spring
2
+
3
+ Flagship framework scaffolding tool for the Strav ecosystem - the Laravel of the Bun ecosystem.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx @strav/spring my-app --web # full-stack with Vue islands
9
+ bunx @strav/spring my-app --api # headless REST API
10
+ bunx @strav/spring my-app # interactive prompt
11
+ ```
12
+
13
+ ## Templates
14
+
15
+ - **api** — Headless REST API with CORS enabled
16
+ - **web** — Full-stack with .strav views, Vue islands, and sessions
17
+
18
+ ## Options
19
+
20
+ ```
21
+ bunx @strav/spring <project-name> [options]
22
+
23
+ --api Headless REST API template
24
+ --web Full-stack template with Vue islands
25
+ --template, -t api|web Alias for --api / --web
26
+ --db <name> Database name (default: project name)
27
+ -h, --help Show help
28
+ ```
29
+
30
+ ## What's scaffolded
31
+
32
+ ```
33
+ my-app/
34
+ ├── app/
35
+ │ ├── controllers/ # HTTP controllers
36
+ │ ├── models/ # Database models (generated from schemas)
37
+ │ ├── middleware/ # Custom middleware
38
+ │ ├── providers/ # Service providers
39
+ │ ├── policies/ # Authorization policies
40
+ │ ├── jobs/ # Queue jobs
41
+ │ └── services/ # Business logic services
42
+ ├── config/ # Configuration files
43
+ ├── database/
44
+ │ ├── schemas/public/ # Schema definitions
45
+ │ ├── migrations/public/ # Generated migrations
46
+ │ ├── seeders/ # Database seeders
47
+ │ └── factories/ # Model factories
48
+ ├── resources/
49
+ │ ├── views/ # .strav templates
50
+ │ ├── css/ # Stylesheets
51
+ │ └── ts/islands/ # Vue.js islands
52
+ ├── routes/ # Route definitions
53
+ ├── tests/ # Test files
54
+ ├── index.ts # Application entry point
55
+ ├── strav.ts # CLI tool
56
+ └── .env # Environment variables
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@strav/spring",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Flagship framework scaffolding tool for the Strav ecosystem.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "strav",
9
+ "bun",
10
+ "framework",
11
+ "scaffold",
12
+ "create",
13
+ "laravel",
14
+ "typescript",
15
+ "vue"
16
+ ],
17
+ "bin": {
18
+ "@strav/spring": "./src/index.ts"
19
+ },
20
+ "files": [
21
+ "src/",
22
+ "package.json",
23
+ "README.md"
24
+ ],
25
+ "dependencies": {
26
+ "@strav/kernel": "0.3.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "latest"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import { select, input } from './prompts.ts'
5
+ import { scaffold, type ScaffoldOptions } from './scaffold.ts'
6
+ import pkg from '../package.json'
7
+
8
+ const VERSION = pkg.version
9
+
10
+ // ── Colors ──────────────────────────────────────────────────────────
11
+
12
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
13
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
14
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`
15
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
16
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`
17
+
18
+ // ── Arg parsing ─────────────────────────────────────────────────────
19
+
20
+ interface ParsedArgs {
21
+ projectName?: string
22
+ template?: 'api' | 'web'
23
+ db?: string
24
+ help?: boolean
25
+ }
26
+
27
+ function parseArgs(): ParsedArgs {
28
+ const args = process.argv.slice(2)
29
+ const result: ParsedArgs = {}
30
+
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i]
33
+
34
+ if (arg === '--help' || arg === '-h') {
35
+ result.help = true
36
+ } else if (arg === '--api') {
37
+ result.template = 'api'
38
+ } else if (arg === '--web') {
39
+ result.template = 'web'
40
+ } else if (arg === '--template' || arg === '-t') {
41
+ const val = args[++i]
42
+ if (val === 'api' || val === 'web') {
43
+ result.template = val
44
+ } else {
45
+ console.error(red(` Invalid template: ${val}. Use "api" or "web".`))
46
+ process.exit(1)
47
+ }
48
+ } else if (arg === '--db') {
49
+ result.db = args[++i]
50
+ } else if (arg && !arg.startsWith('-') && !result.projectName) {
51
+ result.projectName = arg
52
+ }
53
+ }
54
+
55
+ return result
56
+ }
57
+
58
+ function printUsage(): void {
59
+ console.log(`
60
+ ${bold('@strav/spring')} ${dim(`v${VERSION}`)}
61
+ ${dim('The Laravel of the Bun ecosystem')}
62
+
63
+ ${bold('Usage:')}
64
+ bunx @strav/spring ${cyan('<project-name>')} [options]
65
+
66
+ ${bold('Options:')}
67
+ --api Headless REST API template
68
+ --web Full-stack template with Vue islands and views
69
+ --template, -t ${dim('api|web')} Alias for --api / --web
70
+ --db ${dim('<name>')} Database name (default: project name)
71
+ -h, --help Show this help message
72
+
73
+ ${bold('Examples:')}
74
+ bunx @strav/spring my-blog --web
75
+ bunx @strav/spring my-api --api
76
+ bunx @strav/spring my-app ${dim('# interactive prompt')}
77
+ `)
78
+ }
79
+
80
+ function toSnakeCase(name: string): string {
81
+ return name
82
+ .replace(/([A-Z])/g, '_$1')
83
+ .toLowerCase()
84
+ .replace(/[-\s]+/g, '_')
85
+ .replace(/^_+|_+$/g, '')
86
+ .replace(/_+/g, '_')
87
+ }
88
+
89
+ // ── Main ────────────────────────────────────────────────────────────
90
+
91
+ async function main(): Promise<void> {
92
+ const args = parseArgs()
93
+
94
+ if (args.help) {
95
+ printUsage()
96
+ process.exit(0)
97
+ }
98
+
99
+ console.log()
100
+ console.log(` ${bold('@strav/spring')} ${dim(`v${VERSION}`)}`)
101
+ console.log(` ${dim('The Laravel of the Bun ecosystem')}`)
102
+ console.log()
103
+
104
+ // Project name
105
+ if (!args.projectName) {
106
+ printUsage()
107
+ process.exit(1)
108
+ }
109
+
110
+ const projectName = args.projectName
111
+ const root = resolve(projectName)
112
+
113
+ // Validate
114
+ if (existsSync(root)) {
115
+ console.error(red(` Directory "${projectName}" already exists.`))
116
+ process.exit(1)
117
+ }
118
+
119
+ if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
120
+ console.error(
121
+ red(` Invalid project name. Use only letters, numbers, hyphens, and underscores.`)
122
+ )
123
+ process.exit(1)
124
+ }
125
+
126
+ // Template
127
+ let template = args.template
128
+ if (!template) {
129
+ template = (await select('Which template?', [
130
+ { label: 'web', value: 'web', description: 'Full-stack with Vue islands, views, and sessions' },
131
+ { label: 'api', value: 'api', description: 'Headless REST API with CORS enabled' },
132
+ ])) as 'api' | 'web'
133
+ }
134
+
135
+ // Database name
136
+ const defaultDb = toSnakeCase(projectName)
137
+ const dbName = args.db ?? defaultDb
138
+
139
+ console.log()
140
+
141
+ // Scaffold
142
+ const opts: ScaffoldOptions = { projectName, template, dbName }
143
+ await scaffold(root, opts)
144
+ console.log(` ${green('+')} Scaffolded project files`)
145
+
146
+ // Install dependencies
147
+ console.log(` ${dim('...')} Installing dependencies`)
148
+ const install = Bun.spawn(['bun', 'install'], { cwd: root, stdout: 'ignore', stderr: 'pipe' })
149
+ const exitCode = await install.exited
150
+
151
+ if (exitCode !== 0) {
152
+ const stderr = await new Response(install.stderr).text()
153
+ console.error(red(` Failed to install dependencies:`))
154
+ console.error(dim(` ${stderr}`))
155
+ process.exit(1)
156
+ }
157
+
158
+ console.log(` ${green('+')} Installed dependencies`)
159
+
160
+ // Done
161
+ console.log()
162
+ console.log(` ${green('Project created successfully!')}`)
163
+ console.log()
164
+ console.log(` Next steps:`)
165
+ console.log()
166
+ console.log(` ${dim('$')} cd ${projectName}`)
167
+ console.log(` ${dim('$')} bun --hot index.ts`)
168
+ console.log()
169
+ console.log(` ${dim('Then open http://localhost:3000')}`)
170
+ console.log()
171
+ }
172
+
173
+ main().catch(err => {
174
+ console.error(red(` Error: ${err instanceof Error ? err.message : err}`))
175
+ process.exit(1)
176
+ })
package/src/prompts.ts ADDED
@@ -0,0 +1,135 @@
1
+ const ESC = '\x1b'
2
+ const ARROW_UP = `${ESC}[A`
3
+ const ARROW_DOWN = `${ESC}[B`
4
+ const ENTER = '\r'
5
+
6
+ interface Choice {
7
+ label: string
8
+ value: string
9
+ description: string
10
+ }
11
+
12
+ export async function select(message: string, choices: Choice[]): Promise<string> {
13
+ let selected = 0
14
+
15
+ const render = () => {
16
+ // Move cursor up to overwrite previous render (except first time)
17
+ process.stdout.write(`\x1b[${choices.length}A`)
18
+ for (let i = 0; i < choices.length; i++) {
19
+ const prefix = i === selected ? '\x1b[36m>\x1b[0m' : ' '
20
+ const choice = choices[i]!
21
+ const label = i === selected ? `\x1b[1m${choice.label}\x1b[0m` : choice.label
22
+ const desc = `\x1b[2m${choice.description}\x1b[0m`
23
+ process.stdout.write(`\x1b[2K ${prefix} ${label} ${desc}\n`)
24
+ }
25
+ }
26
+
27
+ process.stdout.write(` \x1b[1m${message}\x1b[0m\n`)
28
+ // Print initial lines so render() can overwrite them
29
+ for (const choice of choices) {
30
+ process.stdout.write('\n')
31
+ }
32
+ render()
33
+
34
+ return new Promise(resolve => {
35
+ const stdin = process.stdin
36
+ stdin.setRawMode(true)
37
+ stdin.resume()
38
+ stdin.setEncoding('utf8')
39
+
40
+ let buffer = ''
41
+
42
+ const onData = (data: string) => {
43
+ buffer += data
44
+
45
+ // Check for Ctrl+C
46
+ if (buffer.includes('\x03')) {
47
+ stdin.setRawMode(false)
48
+ stdin.pause()
49
+ stdin.removeListener('data', onData)
50
+ process.stdout.write('\n')
51
+ process.exit(0)
52
+ }
53
+
54
+ // Process escape sequences
55
+ while (buffer.length > 0) {
56
+ if (buffer.startsWith(ARROW_UP)) {
57
+ selected = (selected - 1 + choices.length) % choices.length
58
+ render()
59
+ buffer = buffer.slice(ARROW_UP.length)
60
+ } else if (buffer.startsWith(ARROW_DOWN)) {
61
+ selected = (selected + 1) % choices.length
62
+ render()
63
+ buffer = buffer.slice(ARROW_DOWN.length)
64
+ } else if (buffer.startsWith(ENTER)) {
65
+ stdin.setRawMode(false)
66
+ stdin.pause()
67
+ stdin.removeListener('data', onData)
68
+ resolve(choices[selected]!.value)
69
+ buffer = ''
70
+ return
71
+ } else if (buffer.startsWith(ESC)) {
72
+ // Incomplete escape sequence, wait for more data
73
+ break
74
+ } else {
75
+ // Discard unrecognized input
76
+ buffer = buffer.slice(1)
77
+ }
78
+ }
79
+ }
80
+
81
+ stdin.on('data', onData)
82
+ })
83
+ }
84
+
85
+ export async function input(message: string, defaultValue: string): Promise<string> {
86
+ process.stdout.write(` \x1b[1m${message}\x1b[0m \x1b[2m(${defaultValue})\x1b[0m `)
87
+
88
+ return new Promise(resolve => {
89
+ const stdin = process.stdin
90
+ stdin.setRawMode(true)
91
+ stdin.resume()
92
+ stdin.setEncoding('utf8')
93
+
94
+ let value = ''
95
+
96
+ const onData = (data: string) => {
97
+ for (const char of data) {
98
+ if (char === '\x03') {
99
+ // Ctrl+C
100
+ stdin.setRawMode(false)
101
+ stdin.pause()
102
+ stdin.removeListener('data', onData)
103
+ process.stdout.write('\n')
104
+ process.exit(0)
105
+ }
106
+
107
+ if (char === '\r' || char === '\n') {
108
+ stdin.setRawMode(false)
109
+ stdin.pause()
110
+ stdin.removeListener('data', onData)
111
+ process.stdout.write('\n')
112
+ resolve(value || defaultValue)
113
+ return
114
+ }
115
+
116
+ if (char === '\x7f' || char === '\b') {
117
+ // Backspace
118
+ if (value.length > 0) {
119
+ value = value.slice(0, -1)
120
+ process.stdout.write('\b \b')
121
+ }
122
+ continue
123
+ }
124
+
125
+ // Printable characters
126
+ if (char >= ' ') {
127
+ value += char
128
+ process.stdout.write(char)
129
+ }
130
+ }
131
+ }
132
+
133
+ stdin.on('data', onData)
134
+ })
135
+ }
@@ -0,0 +1,54 @@
1
+ import { readdirSync, mkdirSync, statSync } from 'node:fs'
2
+ import { join, dirname } from 'node:path'
3
+ import pkg from '../package.json'
4
+
5
+ export interface ScaffoldOptions {
6
+ projectName: string
7
+ template: 'api' | 'web'
8
+ dbName: string
9
+ }
10
+
11
+ export async function scaffold(root: string, opts: ScaffoldOptions): Promise<void> {
12
+ const templatesDir = join(import.meta.dir, 'templates')
13
+ const appKey = crypto.randomUUID()
14
+
15
+ const replacements: Record<string, string> = {
16
+ __PROJECT_NAME__: opts.projectName,
17
+ __DB_NAME__: opts.dbName,
18
+ __APP_KEY__: appKey,
19
+ __STRAV_VERSION__: `^${pkg.version}`,
20
+ }
21
+
22
+ // Copy shared files first, then template-specific (may override shared)
23
+ await copyDir(join(templatesDir, 'shared'), root, replacements)
24
+ await copyDir(join(templatesDir, opts.template), root, replacements)
25
+ }
26
+
27
+ async function copyDir(
28
+ srcDir: string,
29
+ destDir: string,
30
+ replacements: Record<string, string>
31
+ ): Promise<void> {
32
+ const entries = readdirSync(srcDir)
33
+
34
+ for (const entry of entries) {
35
+ const srcPath = join(srcDir, entry)
36
+ const destPath = join(destDir, entry.replace(/\.tpl$/, ''))
37
+
38
+ if (statSync(srcPath).isDirectory()) {
39
+ await copyDir(srcPath, destPath, replacements)
40
+ } else {
41
+ mkdirSync(dirname(destPath), { recursive: true })
42
+ const content = await Bun.file(srcPath).text()
43
+ await Bun.write(destPath, applyReplacements(content, replacements))
44
+ }
45
+ }
46
+ }
47
+
48
+ function applyReplacements(content: string, replacements: Record<string, string>): string {
49
+ let result = content
50
+ for (const [placeholder, value] of Object.entries(replacements)) {
51
+ result = result.replaceAll(placeholder, value)
52
+ }
53
+ return result
54
+ }
@@ -0,0 +1,69 @@
1
+ import type { Context } from '@strav/http'
2
+ import { Controller } from './controller.ts'
3
+ import User from '../models/user.ts'
4
+
5
+ export default class UserController extends Controller {
6
+ async index(ctx: Context) {
7
+ const users = await User.all()
8
+ return this.respond(ctx, { users })
9
+ }
10
+
11
+ async show(ctx: Context) {
12
+ const { id } = ctx.params
13
+ const user = await User.find(id)
14
+
15
+ if (!user) {
16
+ return this.notFound(ctx, 'User not found')
17
+ }
18
+
19
+ return this.respond(ctx, { user })
20
+ }
21
+
22
+ async store(ctx: Context) {
23
+ const { email, name, password } = await ctx.request.json()
24
+
25
+ if (!email || !name || !password) {
26
+ return this.error(ctx, 'Email, name, and password are required')
27
+ }
28
+
29
+ const user = await User.create({
30
+ id: crypto.randomUUID(),
31
+ email,
32
+ name,
33
+ password_hash: await Bun.password.hash(password),
34
+ })
35
+
36
+ return this.respond(ctx, { user }, 201)
37
+ }
38
+
39
+ async update(ctx: Context) {
40
+ const { id } = ctx.params
41
+ const user = await User.find(id)
42
+
43
+ if (!user) {
44
+ return this.notFound(ctx, 'User not found')
45
+ }
46
+
47
+ const { email, name } = await ctx.request.json()
48
+
49
+ if (email) user.email = email
50
+ if (name) user.name = name
51
+
52
+ await user.save()
53
+
54
+ return this.respond(ctx, { user })
55
+ }
56
+
57
+ async destroy(ctx: Context) {
58
+ const { id } = ctx.params
59
+ const user = await User.find(id)
60
+
61
+ if (!user) {
62
+ return this.notFound(ctx, 'User not found')
63
+ }
64
+
65
+ await user.delete()
66
+
67
+ return this.respond(ctx, { message: 'User deleted successfully' })
68
+ }
69
+ }
@@ -0,0 +1,10 @@
1
+ import { env } from '@strav/kernel'
2
+
3
+ export default {
4
+ port: env.int('APP_PORT', 3000),
5
+ cors: {
6
+ enabled: true,
7
+ origin: ['http://localhost:3000', 'http://localhost:5173'],
8
+ credentials: true,
9
+ },
10
+ }
@@ -0,0 +1,33 @@
1
+ import 'reflect-metadata'
2
+ import { app } from '@strav/kernel'
3
+ import { router } from '@strav/http'
4
+ import { ConfigProvider, EncryptionProvider } from '@strav/kernel'
5
+ import { DatabaseProvider } from '@strav/database'
6
+ import BaseModel from '@strav/database/orm/base_model'
7
+ import Database from '@strav/database/database/database'
8
+ import Server from '@strav/http/server'
9
+ import { ExceptionHandler } from '@strav/kernel'
10
+
11
+ // Register service providers
12
+ app
13
+ .use(new ConfigProvider())
14
+ .use(new DatabaseProvider())
15
+ .use(new EncryptionProvider())
16
+
17
+ // Boot services (loads config, connects database, derives encryption keys)
18
+ await app.start()
19
+
20
+ // Initialize ORM
21
+ new BaseModel(app.resolve(Database))
22
+
23
+ // Configure router for API
24
+ router.useExceptionHandler(new ExceptionHandler(true))
25
+ router.cors()
26
+
27
+ // Load routes
28
+ await import('./routes/routes')
29
+
30
+ // Start HTTP server
31
+ app.singleton(Server)
32
+ const server = app.resolve(Server)
33
+ server.start(router)
@@ -0,0 +1,24 @@
1
+ import type { Router } from '@strav/http'
2
+ import UserController from '../app/controllers/user_controller.ts'
3
+
4
+ export default function (router: Router) {
5
+ // Health check endpoint
6
+ router.get('/health', async (ctx) => {
7
+ return ctx.json({
8
+ status: 'ok',
9
+ timestamp: new Date().toISOString(),
10
+ app: '__PROJECT_NAME__',
11
+ version: '0.1.0'
12
+ })
13
+ })
14
+
15
+ // API routes
16
+ router.group('/api/v1', () => {
17
+ // User resource routes
18
+ router.get('/users', [UserController, 'index'])
19
+ router.get('/users/:id', [UserController, 'show'])
20
+ router.post('/users', [UserController, 'store'])
21
+ router.put('/users/:id', [UserController, 'update'])
22
+ router.delete('/users/:id', [UserController, 'destroy'])
23
+ })
24
+ }
@@ -0,0 +1,14 @@
1
+ APP_ENV=local
2
+ APP_KEY=__APP_KEY__
3
+ APP_DEBUG=true
4
+ APP_URL=http://localhost:3000
5
+ APP_PORT=3000
6
+
7
+ DB_HOST=127.0.0.1
8
+ DB_PORT=5432
9
+ DB_USER=liva
10
+ DB_PASSWORD=password1234
11
+ DB_DATABASE=__DB_NAME__
12
+
13
+ SESSION_SECRET=__APP_KEY__
14
+ SESSION_COOKIE_NAME=session
@@ -0,0 +1,15 @@
1
+ import type { Context } from '@strav/http'
2
+
3
+ export abstract class Controller {
4
+ protected async respond<T>(ctx: Context, data: T, status = 200) {
5
+ return ctx.json(data, status)
6
+ }
7
+
8
+ protected async error(ctx: Context, message: string, status = 400) {
9
+ return ctx.json({ error: message }, status)
10
+ }
11
+
12
+ protected async notFound(ctx: Context, message = 'Not found') {
13
+ return ctx.json({ error: message }, 404)
14
+ }
15
+ }
@@ -0,0 +1,30 @@
1
+ import { Model, column } from '@strav/database'
2
+
3
+ export default class User extends Model {
4
+ @column({ isPrimary: true })
5
+ declare id: string
6
+
7
+ @column()
8
+ declare email: string
9
+
10
+ @column()
11
+ declare name: string
12
+
13
+ @column()
14
+ declare password_hash: string
15
+
16
+ @column()
17
+ declare email_verified_at: Date | null
18
+
19
+ @column()
20
+ declare remember_token: string | null
21
+
22
+ @column()
23
+ declare created_at: Date
24
+
25
+ @column()
26
+ declare updated_at: Date
27
+
28
+ @column()
29
+ declare deleted_at: Date | null
30
+ }
@@ -0,0 +1,10 @@
1
+ import { env } from '@strav/kernel'
2
+
3
+ export default {
4
+ name: '__PROJECT_NAME__',
5
+ env: env('APP_ENV', 'production'),
6
+ debug: env.bool('APP_DEBUG', false),
7
+ url: env('APP_URL', 'http://localhost:3000'),
8
+ port: env.int('APP_PORT', 3000),
9
+ key: env('APP_KEY'),
10
+ }
@@ -0,0 +1,9 @@
1
+ import { env } from '@strav/kernel'
2
+
3
+ export default {
4
+ host: env('DB_HOST', 'localhost'),
5
+ port: env.int('DB_PORT', 5432),
6
+ database: env('DB_DATABASE', '__DB_NAME__'),
7
+ username: env('DB_USER', 'postgres'),
8
+ password: env('DB_PASSWORD', ''),
9
+ }
@@ -0,0 +1,5 @@
1
+ import { env } from '@strav/kernel'
2
+
3
+ export default {
4
+ key: env('APP_KEY'),
5
+ }