bunigniter 0.2.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/LICENSE +21 -0
  4. package/dist/README.md +229 -0
  5. package/dist/base/controller.ts +324 -0
  6. package/dist/base/index.ts +5 -0
  7. package/dist/base/service.ts +21 -0
  8. package/dist/cli/index.ts +318 -0
  9. package/dist/cli/list-routes.ts +72 -0
  10. package/dist/cli/repl.ts +461 -0
  11. package/dist/cli/templates.ts +283 -0
  12. package/dist/client/index.ts +159 -0
  13. package/dist/db/drizzle.ts +550 -0
  14. package/dist/db/validators.ts +229 -0
  15. package/dist/edge-builder.ts +120 -0
  16. package/dist/edge.ts +69 -0
  17. package/dist/helpers/cache.ts +173 -0
  18. package/dist/helpers/cors.ts +103 -0
  19. package/dist/helpers/csrf.ts +155 -0
  20. package/dist/helpers/debug.ts +158 -0
  21. package/dist/helpers/env.ts +147 -0
  22. package/dist/helpers/handler.ts +158 -0
  23. package/dist/helpers/http.ts +194 -0
  24. package/dist/helpers/image.ts +217 -0
  25. package/dist/helpers/jwt.ts +147 -0
  26. package/dist/helpers/logger.ts +96 -0
  27. package/dist/helpers/mail.ts +272 -0
  28. package/dist/helpers/middleware-loader.ts +116 -0
  29. package/dist/helpers/middleware.ts +57 -0
  30. package/dist/helpers/modules.ts +115 -0
  31. package/dist/helpers/openapi.ts +140 -0
  32. package/dist/helpers/pagination.ts +159 -0
  33. package/dist/helpers/queue.ts +186 -0
  34. package/dist/helpers/request-context.ts +13 -0
  35. package/dist/helpers/request.ts +376 -0
  36. package/dist/helpers/schedule.ts +173 -0
  37. package/dist/helpers/session-middleware.ts +89 -0
  38. package/dist/helpers/session.ts +286 -0
  39. package/dist/helpers/sse.ts +90 -0
  40. package/dist/helpers/throttle.ts +156 -0
  41. package/dist/helpers/upload.ts +417 -0
  42. package/dist/helpers/validator.ts +287 -0
  43. package/dist/helpers/ws.ts +123 -0
  44. package/dist/index.ts +221 -0
  45. package/dist/package.json +70 -0
  46. package/dist/router/file-router.ts +541 -0
  47. package/dist/router/server-router.ts +103 -0
  48. package/dist/view/page.ts +96 -0
  49. package/dist/view/renderer.tsx +390 -0
  50. package/dist/view/view-response.ts +10 -0
  51. package/package.json +70 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Shared template renderer for all `make:*` CLI commands.
3
+ * Single source of truth — update here to affect all generators.
4
+ */
5
+ export function render(template: string, data: Record<string, any>): string {
6
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => String(data[key] ?? ''))
7
+ }
8
+
9
+ /** Route controller template. */
10
+ export function controller(name: string, prefix: string): string {
11
+ return render(`/**
12
+ * {{Name}} controller
13
+ *
14
+ * GET {{prefix}}/{{name}}
15
+ * POST {{prefix}}/{{name}}
16
+ * PUT {{prefix}}/{{name}}/:id
17
+ * DELETE {{prefix}}/{{name}}/:id
18
+ */
19
+ import { Controller } from 'bunigniter'
20
+
21
+ export class {{Name}} extends Controller {
22
+ async index() {
23
+ return this.json({ message: '{{name}} index' })
24
+ }
25
+
26
+ async show(id: number) {
27
+ return this.json({ message: '{{name}} show', id })
28
+ }
29
+
30
+ async create() {
31
+ const v = this.validate(this.body, {
32
+ // name: 'required'
33
+ })
34
+ if (v.fails()) return this.badRequest(v.errors)
35
+ return this.json({ message: '{{name}} created' }, 201)
36
+ }
37
+
38
+ async update(id: number) {
39
+ return this.json({ message: '{{name}} updated', id })
40
+ }
41
+
42
+ async destroy(id: number) {
43
+ return this.json({ message: '{{name}} deleted', id })
44
+ }
45
+ }
46
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1), prefix })
47
+ }
48
+
49
+ /** DB schema template. */
50
+ export function model(name: string, columns: string): string {
51
+ const tableName = name.toLowerCase() + 's'
52
+ const cols = columns.split(',').filter(Boolean).map((c: string) => {
53
+ const [colName, colType] = c.trim().split(':')
54
+ return { name: colName || 'id', type: colType || 'string' }
55
+ })
56
+ const fieldDefs = cols.map((c: any) => {
57
+ if (c.type === 'number' || c.type === 'integer') {
58
+ return ` ${c.name}: integer('${c.name}'),`
59
+ }
60
+ return ` ${c.name}: text('${c.name}'),`
61
+ }).join('\n')
62
+
63
+ return `import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
64
+
65
+ export const ${tableName} = sqliteTable('${tableName}', {
66
+ id: integer('id').primaryKey({ autoIncrement: true }),
67
+ ${fieldDefs}
68
+ createdAt: text('created_at').default('CURRENT_TIMESTAMP'),
69
+ })
70
+ `
71
+ }
72
+
73
+ /** Migration file template. */
74
+ export function migration(name: string): string {
75
+ const timestamp = Date.now()
76
+ const tableName = name.toLowerCase().replace(/^create_|^add_|^drop_/, '').replace(/_table$/, '') + 's'
77
+ const isCreate = name.toLowerCase().startsWith('create')
78
+
79
+ return `-- Migration: ${name}
80
+ -- Generated at: ${new Date().toISOString()}
81
+
82
+ ${isCreate ? `CREATE TABLE IF NOT EXISTS ${tableName} (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ created_at TEXT DEFAULT (datetime('now')),
85
+ updated_at TEXT DEFAULT (datetime('now'))
86
+ );` : `-- Add your migration SQL here
87
+ -- ALTER TABLE ${tableName} ADD COLUMN ...;`}
88
+ `
89
+ }
90
+
91
+ /** Middleware template. */
92
+ export function middleware(name: string): string {
93
+ return render(`/**
94
+ * {{name}} middleware
95
+ */
96
+ import { defineMiddleware } from 'bunigniter'
97
+
98
+ export default defineMiddleware(async (c, next) => {
99
+ const start = performance.now()
100
+ await next()
101
+ const duration = Math.round((performance.now() - start) * 100) / 100
102
+ c.set.headers ??= {}
103
+ c.set.headers['X-{{Name}}-Time'] = \`\${duration}ms\`
104
+ })
105
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
106
+ }
107
+
108
+ /** CLI command template. */
109
+ export function command(name: string): string {
110
+ return render(`/**
111
+ * {{name}} command — scaffolded CLI command
112
+ */
113
+ import type { CommandArgs } from '../cli/types'
114
+
115
+ export default {
116
+ name: '{{name}}',
117
+ desc: 'Description for {{name}}',
118
+ async run(args: CommandArgs) {
119
+ console.log('Running {{name}} with args:', args)
120
+ }
121
+ }
122
+ `, { name })
123
+ }
124
+
125
+ /** Test file template. */
126
+ export function test(name: string): string {
127
+ return render(`/**
128
+ * {{name}} tests
129
+ */
130
+ import { describe, it, expect } from 'vitest'
131
+
132
+ describe('{{Name}}', () => {
133
+ it('should work', () => {
134
+ expect(1 + 1).toBe(2)
135
+ })
136
+ })
137
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
138
+ }
139
+
140
+ /** Queue job template. */
141
+ export function job(name: string): string {
142
+ return render(`/**
143
+ * {{name}} job — queue worker
144
+ */
145
+ import type { Job } from 'bunigniter/helpers/queue'
146
+
147
+ export default async function (job: Job) {
148
+ const { data } = job
149
+ console.log('Processing {{name}} job:', data)
150
+ // Add your job logic here
151
+ }
152
+ `, { name })
153
+ }
154
+
155
+ /** Mail class template. */
156
+ export function mail(name: string): string {
157
+ return render(`/**
158
+ * {{name}} mail
159
+ */
160
+ export async function send{{Name}}(to: string, data: Record<string, any> = {}) {
161
+ // const { mail } = await import('bunigniter/helpers/mail')
162
+ // await mail.send({ to, subject: '{{Name}}', html: \`<h1>\${data.title}</h1>\` })
163
+ console.log('Sending {{name}} mail to', to, data)
164
+ }
165
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
166
+ }
167
+
168
+ /** Seeder template. */
169
+ export function seeder(name: string): string {
170
+ return render(`/**
171
+ * {{name}} seeder
172
+ */
173
+ export default async function seed(ctx: any) {
174
+ const { db } = ctx
175
+ // await db.query('INSERT INTO {{name}} (name) VALUES (?)', ['Sample'])
176
+ console.log('Seeding {{name}}...')
177
+ }
178
+ `, { name })
179
+ }
180
+
181
+ /** Event template. */
182
+ export function eventTemplate(name: string): string {
183
+ return render(`/**
184
+ * {{name}} event
185
+ */
186
+ export class {{Name}} {
187
+ constructor(public readonly data: any) {}
188
+ }
189
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
190
+ }
191
+
192
+ /** Listener template. */
193
+ export function listener(name: string): string {
194
+ return render(`/**
195
+ * {{name}} listener
196
+ */
197
+ export default async function handle{{Name}}(event: any) {
198
+ console.log('Handling {{name}}:', event.data)
199
+ }
200
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
201
+ }
202
+
203
+ /** Service provider template. */
204
+ export function provider(name: string): string {
205
+ return render(`/**
206
+ * {{name}} service provider
207
+ */
208
+ export default {
209
+ register() {
210
+ // Register bindings here
211
+ },
212
+ boot() {
213
+ // Run after all providers are registered
214
+ },
215
+ }
216
+ `, { name })
217
+ }
218
+
219
+ /** Policy template. */
220
+ export function policy(name: string): string {
221
+ return render(`/**
222
+ * {{name}} policy
223
+ */
224
+ export class {{Name}}Policy {
225
+ view(user: any, resource: any) { return true }
226
+ create(user: any) { return true }
227
+ update(user: any, resource: any) { return user.id === resource.user_id }
228
+ delete(user: any, resource: any) { return user.id === resource.user_id }
229
+ }
230
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
231
+ }
232
+
233
+ /** Form request template. */
234
+ export function formRequest(name: string): string {
235
+ return render(`/**
236
+ * {{name}} form request
237
+ */
238
+ import { z } from 'zod'
239
+
240
+ export const {{Name}}Schema = z.object({
241
+ // name: z.string().min(2),
242
+ // email: z.string().email(),
243
+ })
244
+
245
+ export type {{Name}}Data = z.infer<typeof {{Name}}Schema>
246
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
247
+ }
248
+
249
+ /** API resource template. */
250
+ export function resource(name: string): string {
251
+ return render(`/**
252
+ * {{name}} API resource
253
+ */
254
+ export interface {{Name}} {
255
+ id: number
256
+ // Add fields here
257
+ createdAt: string
258
+ updatedAt: string
259
+ }
260
+
261
+ export function {{name}}ToJson(item: {{Name}}): Record<string, any> {
262
+ return {
263
+ id: item.id,
264
+ // Map fields here
265
+ createdAt: item.createdAt,
266
+ }
267
+ }
268
+ `, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
269
+ }
270
+
271
+ /** Validation rule template. */
272
+ export function rule(name: string): string {
273
+ return render(`/**
274
+ * {{name}} validation rule
275
+ */
276
+ export function {{name}}(value: any, params?: string): string | null {
277
+ if (!value) return null
278
+ // Add validation logic
279
+ // return 'Validation failed'
280
+ return null
281
+ }
282
+ `, { name })
283
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Typed Fetch Client — Void-style type-safe API client.
3
+ *
4
+ * Generates a type-safe fetch client from route definitions.
5
+ * In the browser, uses `fetch`. In the server, calls `app.fetch()` directly.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { api } from 'nexusts/client'
10
+ *
11
+ * // GET: fully typed response
12
+ * const users = await api.get('/api/users')
13
+ *
14
+ * // POST: typed body + response
15
+ * const created = await api.post('/api/users', { name: 'Alice', email: 'a@b.com' })
16
+ *
17
+ * // With params
18
+ * const user = await api.get('/api/users/:id', { params: { id: '42' } })
19
+ *
20
+ * // With query
21
+ * const result = await api.get('/api/search', { query: { q: 'hello' } })
22
+ * ```
23
+ */
24
+
25
+ export interface FetchOptions {
26
+ /** HTTP method. Default: GET */
27
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
28
+
29
+ /** Request body (auto-serialized to JSON). */
30
+ body?: any
31
+
32
+ /** Query parameters. */
33
+ query?: Record<string, string | number | boolean | undefined>
34
+
35
+ /** URL path parameters (e.g. `{ id: '42' }` for /:id). */
36
+ params?: Record<string, string | number>
37
+
38
+ /** Additional headers. */
39
+ headers?: Record<string, string>
40
+
41
+ /** Base URL. Default: '' (same origin). */
42
+ baseURL?: string
43
+
44
+ /** Abort signal. */
45
+ signal?: AbortSignal
46
+
47
+ /** Timeout in ms. */
48
+ timeout?: number
49
+ }
50
+
51
+ export interface FetchError {
52
+ status: number
53
+ data: any
54
+ response: Response
55
+ }
56
+
57
+ /**
58
+ * Typed fetch helper.
59
+ *
60
+ * @param path - URL path (supports `:param` placeholders)
61
+ * @param options - Request options
62
+ * @returns Parsed response body
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import { fetch } from 'nexusts/client'
67
+ * const users = await fetch('/api/users')
68
+ * ```
69
+ */
70
+ export async function fetch<T = any>(path: string, options: FetchOptions = {}): Promise<T> {
71
+ const { method = 'GET', body, query, params, headers: extraHeaders, baseURL = '', signal, timeout } = options
72
+
73
+ // Interpolate path params
74
+ let resolvedPath = path
75
+ if (params) {
76
+ for (const [key, value] of Object.entries(params)) {
77
+ resolvedPath = resolvedPath.replace(`:${key}`, String(value))
78
+ }
79
+ }
80
+
81
+ // Build URL
82
+ const url = new URL(resolvedPath, baseURL || 'http://localhost')
83
+ if (query) {
84
+ for (const [key, value] of Object.entries(query)) {
85
+ if (value !== undefined && value !== '') {
86
+ url.searchParams.set(key, String(value))
87
+ }
88
+ }
89
+ }
90
+
91
+ // Build request
92
+ const headers: Record<string, string> = {
93
+ ...(extraHeaders ?? {}),
94
+ }
95
+ if (body && method !== 'GET') {
96
+ headers['content-type'] = 'application/json'
97
+ }
98
+
99
+ const requestInit: RequestInit = {
100
+ method,
101
+ headers,
102
+ signal,
103
+ ...(body && method !== 'GET' ? { body: JSON.stringify(body) } : {}),
104
+ }
105
+
106
+ // Timeout handling
107
+ let timeoutId: Timer | undefined
108
+ if (timeout) {
109
+ const controller = new AbortController()
110
+ timeoutId = setTimeout(() => controller.abort(), timeout)
111
+ requestInit.signal = controller.signal
112
+ }
113
+
114
+ try {
115
+ const response = await fetch(url.toString().replace(/^http:\/\/localhost/, ''), requestInit)
116
+
117
+ if (!response.ok) {
118
+ const error: FetchError = {
119
+ status: response.status,
120
+ data: await response.json().catch(() => null),
121
+ response,
122
+ }
123
+ throw error
124
+ }
125
+
126
+ // Handle empty responses
127
+ const contentType = response.headers.get('content-type') ?? ''
128
+ if (contentType.includes('application/json')) {
129
+ return response.json()
130
+ }
131
+ if (contentType.includes('text/')) {
132
+ return response.text() as unknown as T
133
+ }
134
+ return undefined as unknown as T
135
+ } finally {
136
+ if (timeoutId) clearTimeout(timeoutId)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Convenience methods matching Void's fetch pattern.
142
+ */
143
+ export const api = {
144
+ get<T = any>(path: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
145
+ return fetch<T>(path, { ...options, method: 'GET' })
146
+ },
147
+ post<T = any>(path: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
148
+ return fetch<T>(path, { ...options, method: 'POST', body })
149
+ },
150
+ put<T = any>(path: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
151
+ return fetch<T>(path, { ...options, method: 'PUT', body })
152
+ },
153
+ delete<T = any>(path: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
154
+ return fetch<T>(path, { ...options, method: 'DELETE' })
155
+ },
156
+ patch<T = any>(path: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
157
+ return fetch<T>(path, { ...options, method: 'PATCH', body })
158
+ },
159
+ }