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,159 @@
1
+ /**
2
+ * Pagination — CodeIgniter-style pagination helper.
3
+ *
4
+ * Wraps query results with pagination metadata.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const result = await this.db.paginate('SELECT * FROM users', { page: 1, perPage: 20 })
9
+ * // → { data: User[], total: 100, page: 1, perPage: 20, pages: 5 }
10
+ * ```
11
+ */
12
+ export interface PaginateOptions {
13
+ /** Current page (1-indexed). Default: 1 */
14
+ page?: number
15
+
16
+ /** Items per page. Default: 20 */
17
+ perPage?: number
18
+
19
+ /** Base URL for generated links. */
20
+ baseUrl?: string
21
+ }
22
+
23
+ export interface PaginateResult<T = any> {
24
+ /** Page items. */
25
+ data: T[]
26
+
27
+ /** Total items across all pages. */
28
+ total: number
29
+
30
+ /** Current page number. */
31
+ page: number
32
+
33
+ /** Items per page. */
34
+ perPage: number
35
+
36
+ /** Total pages. */
37
+ pages: number
38
+
39
+ /** Is this the first page? */
40
+ firstPage: boolean
41
+
42
+ /** Is this the last page? */
43
+ lastPage: boolean
44
+
45
+ /** Count of items on current page. */
46
+ count: number
47
+
48
+ /** Previous page number (null if first). */
49
+ prevPage: number | null
50
+
51
+ /** Next page number (null if last). */
52
+ nextPage: number | null
53
+
54
+ /** Generated pagination links (simple HTML). */
55
+ links: string
56
+ }
57
+
58
+ /**
59
+ * Wrap an array with pagination metadata.
60
+ *
61
+ * @param data - Items for the current page
62
+ * @param total - Total items across all pages
63
+ * @param options - Pagination options
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * const result = paginate(rows, totalCount, { page: 1, perPage: 20 })
68
+ * ```
69
+ */
70
+ export function paginate<T = any>(
71
+ data: T[],
72
+ total: number,
73
+ options: PaginateOptions = {}
74
+ ): PaginateResult<T> {
75
+ const page = Math.max(1, options.page ?? 1)
76
+ const perPage = Math.max(1, options.perPage ?? 20)
77
+ const pages = Math.max(1, Math.ceil(total / perPage))
78
+ const currentPage = Math.min(page, pages)
79
+
80
+ return {
81
+ data,
82
+ total,
83
+ page: currentPage,
84
+ perPage,
85
+ pages,
86
+ firstPage: currentPage === 1,
87
+ lastPage: currentPage === pages,
88
+ count: data.length,
89
+ prevPage: currentPage > 1 ? currentPage - 1 : null,
90
+ nextPage: currentPage < pages ? currentPage + 1 : null,
91
+ links: generateLinks(total, currentPage, pages, options.baseUrl ?? ''),
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Generate simple HTML pagination links.
97
+ */
98
+ function generateLinks(total: number, page: number, pages: number, baseUrl: string): string {
99
+ if (pages <= 1) return ''
100
+
101
+ const prev = page > 1 ? `<a href="${baseUrl}?page=${page - 1}">&laquo; Prev</a>` : '<span>&laquo; Prev</span>'
102
+ const next = page < pages ? `<a href="${baseUrl}?page=${page + 1}">Next &raquo;</a>` : '<span>Next &raquo;</span>'
103
+
104
+ let numbers = ''
105
+ const start = Math.max(1, page - 2)
106
+ const end = Math.min(pages, page + 2)
107
+
108
+ for (let i = start; i <= end; i++) {
109
+ if (i === page) {
110
+ numbers += `<strong>${i}</strong> `
111
+ } else {
112
+ numbers += `<a href="${baseUrl}?page=${i}">${i}</a> `
113
+ }
114
+ }
115
+
116
+ return `<div class="pagination">${prev} ${numbers} ${next}</div>`
117
+ }
118
+
119
+ /**
120
+ * Add paginate helper to DbClient prototype.
121
+ * Usage: `this.db.paginate('SELECT * FROM users', [], { page: 1, perPage: 20 })`
122
+ */
123
+ export function addPaginationToDb(db: any): void {
124
+ if (!db || db._paginateAdded) return
125
+ db._paginateAdded = true
126
+
127
+ /**
128
+ * Execute a paginated query.
129
+ *
130
+ * @param sql - SQL query (without LIMIT/OFFSET)
131
+ * @param params - Query parameters
132
+ * @param options - Pagination options
133
+ */
134
+ db.paginate = async function <T = any>(
135
+ sql: string,
136
+ params: unknown[] = [],
137
+ options: PaginateOptions = {}
138
+ ): Promise<PaginateResult<T>> {
139
+ const page = Math.max(1, options.page ?? 1)
140
+ const perPage = Math.max(1, options.perPage ?? 20)
141
+ const offset = (page - 1) * perPage
142
+
143
+ // Count total
144
+ const countSql = `SELECT count(*) as count FROM (${sql}) _count`
145
+ const countResult = await this.query(countSql, params)
146
+ const total = Number(countResult.rows[0]?.count ?? 0)
147
+
148
+ // Fetch page
149
+ const pageSql = dialectLimitOffset(sql, perPage, offset)
150
+ const dataResult = await this.query<T>(pageSql, params)
151
+
152
+ return paginate(dataResult.rows, total, { page, perPage })
153
+ }
154
+ }
155
+
156
+ /** Add LIMIT/OFFSET for different dialects. */
157
+ function dialectLimitOffset(sql: string, limit: number, offset: number): string {
158
+ return `${sql} LIMIT ${limit} OFFSET ${offset}`
159
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Queue — simple in-memory job queue.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // In a controller
7
+ * this.queue.dispatch('send_email', { to: 'user@test.com', template: 'welcome' })
8
+ *
9
+ * // Process jobs
10
+ * this.queue.process('send_email', async (job) => {
11
+ * await mail.send(job.data)
12
+ * })
13
+ * ```
14
+ */
15
+ export interface QueueOptions {
16
+ /** Max concurrent workers per queue. Default: 5 */
17
+ maxConcurrency?: number
18
+
19
+ /** Poll interval in ms when idle. Default: 1000 */
20
+ pollInterval?: number
21
+ }
22
+
23
+ export interface Job<T = any> {
24
+ id: string
25
+ name: string
26
+ data: T
27
+ attempts: number
28
+ maxAttempts: number
29
+ createdAt: number
30
+ }
31
+
32
+ type JobHandler<T = any> = (job: Job<T>) => Promise<void>
33
+
34
+ interface QueueState {
35
+ pending: Job[]
36
+ processing: Set<string>
37
+ handlers: Map<string, JobHandler>
38
+ running: boolean
39
+ }
40
+
41
+ /** In-memory queue store. */
42
+ const queues = new Map<string, QueueState>()
43
+
44
+ /**
45
+ * Queue service — dispatch and process background jobs.
46
+ *
47
+ * Usage in a Controller:
48
+ * ```ts
49
+ * this.queue.dispatch('send_email', { to: 'user@test.com' })
50
+ * ```
51
+ */
52
+ export class Queue {
53
+ private options: QueueOptions
54
+
55
+ constructor(options: QueueOptions = {}) {
56
+ this.options = {
57
+ maxConcurrency: options.maxConcurrency ?? 5,
58
+ pollInterval: options.pollInterval ?? 1000,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Dispatch a job to a queue.
64
+ *
65
+ * @param name - Queue name
66
+ * @param data - Job payload
67
+ * @param maxAttempts - Max retry attempts. Default: 3
68
+ * @returns Job ID
69
+ */
70
+ dispatch(name: string, data: any, maxAttempts = 3): string {
71
+ const state = getOrCreateQueue(name)
72
+ const job: Job = {
73
+ id: crypto.randomUUID(),
74
+ name,
75
+ data,
76
+ attempts: 0,
77
+ maxAttempts,
78
+ createdAt: Date.now(),
79
+ }
80
+ state.pending.push(job)
81
+ return job.id
82
+ }
83
+
84
+ /**
85
+ * Register a handler for a queue.
86
+ * Starts processing when a handler is registered.
87
+ */
88
+ process(name: string, handler: JobHandler): void {
89
+ const state = getOrCreateQueue(name)
90
+ state.handlers.set(name, handler)
91
+ startProcessing(name, state, this.options)
92
+ }
93
+
94
+ /**
95
+ * Get queue status.
96
+ */
97
+ status(name: string): { pending: number; processing: number } {
98
+ const state = queues.get(name)
99
+ if (!state) return { pending: 0, processing: 0 }
100
+ return {
101
+ pending: state.pending.length,
102
+ processing: state.processing.size,
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Get all queue names.
108
+ */
109
+ get queues(): string[] {
110
+ return [...queues.keys()]
111
+ }
112
+ }
113
+
114
+ function getOrCreateQueue(name: string): QueueState {
115
+ let state = queues.get(name)
116
+ if (!state) {
117
+ state = {
118
+ pending: [],
119
+ processing: new Set(),
120
+ handlers: new Map(),
121
+ running: false,
122
+ }
123
+ queues.set(name, state)
124
+ }
125
+ return state
126
+ }
127
+
128
+ function startProcessing(name: string, state: QueueState, options: QueueOptions): void {
129
+ if (state.running) return
130
+ state.running = true
131
+
132
+ const processNext = async () => {
133
+ if (!state.running) return
134
+
135
+ // Check concurrency
136
+ if (state.processing.size >= (options.maxConcurrency ?? 5)) {
137
+ setTimeout(processNext, options.pollInterval ?? 1000)
138
+ return
139
+ }
140
+
141
+ const job = state.pending.shift()
142
+ if (!job) {
143
+ setTimeout(processNext, options.pollInterval ?? 1000)
144
+ return
145
+ }
146
+
147
+ const handler = state.handlers.get(name)
148
+ if (!handler) {
149
+ // No handler registered — put job back
150
+ state.pending.unshift(job)
151
+ setTimeout(processNext, options.pollInterval ?? 1000)
152
+ return
153
+ }
154
+
155
+ state.processing.add(job.id)
156
+ try {
157
+ await handler(job)
158
+ } catch (err) {
159
+ job.attempts++
160
+ if (job.attempts < job.maxAttempts) {
161
+ // Retry with exponential backoff
162
+ const delay = Math.min(1000 * Math.pow(2, job.attempts), 30000)
163
+ setTimeout(() => {
164
+ state.pending.push(job)
165
+ }, delay)
166
+ } else {
167
+ console.error(`[queue] Job ${job.name}/${job.id} failed after ${job.attempts} attempts:`, err)
168
+ }
169
+ } finally {
170
+ state.processing.delete(job.id)
171
+ }
172
+
173
+ // Process next job immediately
174
+ setImmediate(processNext)
175
+ }
176
+
177
+ processNext()
178
+ }
179
+
180
+ let _queueInstance: Queue | null = null
181
+ export function createQueue(options?: QueueOptions): Queue {
182
+ if (!_queueInstance) {
183
+ _queueInstance = new Queue(options)
184
+ }
185
+ return _queueInstance
186
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Request Context — global holder for current request context.
3
+ * Used by DbClient to log SQL queries to the debug toolbar.
4
+ */
5
+ let currentCtx: any = null
6
+
7
+ export function setRequestContext(ctx: any): void {
8
+ currentCtx = ctx
9
+ }
10
+
11
+ export function getRequestContext(): any {
12
+ return currentCtx
13
+ }