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.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +229 -0
- package/dist/base/controller.ts +324 -0
- package/dist/base/index.ts +5 -0
- package/dist/base/service.ts +21 -0
- package/dist/cli/index.ts +318 -0
- package/dist/cli/list-routes.ts +72 -0
- package/dist/cli/repl.ts +461 -0
- package/dist/cli/templates.ts +283 -0
- package/dist/client/index.ts +159 -0
- package/dist/db/drizzle.ts +550 -0
- package/dist/db/validators.ts +229 -0
- package/dist/edge-builder.ts +120 -0
- package/dist/edge.ts +69 -0
- package/dist/helpers/cache.ts +173 -0
- package/dist/helpers/cors.ts +103 -0
- package/dist/helpers/csrf.ts +155 -0
- package/dist/helpers/debug.ts +158 -0
- package/dist/helpers/env.ts +147 -0
- package/dist/helpers/handler.ts +158 -0
- package/dist/helpers/http.ts +194 -0
- package/dist/helpers/image.ts +217 -0
- package/dist/helpers/jwt.ts +147 -0
- package/dist/helpers/logger.ts +96 -0
- package/dist/helpers/mail.ts +272 -0
- package/dist/helpers/middleware-loader.ts +116 -0
- package/dist/helpers/middleware.ts +57 -0
- package/dist/helpers/modules.ts +115 -0
- package/dist/helpers/openapi.ts +140 -0
- package/dist/helpers/pagination.ts +159 -0
- package/dist/helpers/queue.ts +186 -0
- package/dist/helpers/request-context.ts +13 -0
- package/dist/helpers/request.ts +376 -0
- package/dist/helpers/schedule.ts +173 -0
- package/dist/helpers/session-middleware.ts +89 -0
- package/dist/helpers/session.ts +286 -0
- package/dist/helpers/sse.ts +90 -0
- package/dist/helpers/throttle.ts +156 -0
- package/dist/helpers/upload.ts +417 -0
- package/dist/helpers/validator.ts +287 -0
- package/dist/helpers/ws.ts +123 -0
- package/dist/index.ts +221 -0
- package/dist/package.json +70 -0
- package/dist/router/file-router.ts +541 -0
- package/dist/router/server-router.ts +103 -0
- package/dist/view/page.ts +96 -0
- package/dist/view/renderer.tsx +390 -0
- package/dist/view/view-response.ts +10 -0
- 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}">« Prev</a>` : '<span>« Prev</span>'
|
|
102
|
+
const next = page < pages ? `<a href="${baseUrl}?page=${page + 1}">Next »</a>` : '<span>Next »</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
|
+
}
|