@kava/kava-api-core 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 (59) hide show
  1. package/bun.lock +160 -0
  2. package/dist/auth.util.d.ts +23 -0
  3. package/dist/auth.util.js +175 -0
  4. package/dist/auth.util.js.map +1 -0
  5. package/dist/context.util.d.ts +7 -0
  6. package/dist/context.util.js +11 -0
  7. package/dist/context.util.js.map +1 -0
  8. package/dist/controller.util.d.ts +118 -0
  9. package/dist/controller.util.js +144 -0
  10. package/dist/controller.util.js.map +1 -0
  11. package/dist/conversion.util.d.ts +8 -0
  12. package/dist/conversion.util.js +52 -0
  13. package/dist/conversion.util.js.map +1 -0
  14. package/dist/db.util.d.ts +80 -0
  15. package/dist/db.util.js +166 -0
  16. package/dist/db.util.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +14 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/logger.util.d.ts +30 -0
  21. package/dist/logger.util.js +117 -0
  22. package/dist/logger.util.js.map +1 -0
  23. package/dist/mail.util.d.ts +21 -0
  24. package/dist/mail.util.js +53 -0
  25. package/dist/mail.util.js.map +1 -0
  26. package/dist/middleware.util.d.ts +263 -0
  27. package/dist/middleware.util.js +233 -0
  28. package/dist/middleware.util.js.map +1 -0
  29. package/dist/model.util.d.ts +204 -0
  30. package/dist/model.util.js +1495 -0
  31. package/dist/model.util.js.map +1 -0
  32. package/dist/permission.util.d.ts +38 -0
  33. package/dist/permission.util.js +91 -0
  34. package/dist/permission.util.js.map +1 -0
  35. package/dist/route.util.d.ts +1 -0
  36. package/dist/route.util.js +12 -0
  37. package/dist/route.util.js.map +1 -0
  38. package/dist/storage.util.d.ts +56 -0
  39. package/dist/storage.util.js +82 -0
  40. package/dist/storage.util.js.map +1 -0
  41. package/dist/validation.util.d.ts +7 -0
  42. package/dist/validation.util.js +237 -0
  43. package/dist/validation.util.js.map +1 -0
  44. package/package.json +34 -0
  45. package/src/auth.util.ts +242 -0
  46. package/src/context.util.ts +17 -0
  47. package/src/controller.util.ts +237 -0
  48. package/src/conversion.util.ts +65 -0
  49. package/src/db.util.ts +405 -0
  50. package/src/index.ts +13 -0
  51. package/src/logger.util.ts +170 -0
  52. package/src/mail.util.ts +86 -0
  53. package/src/middleware.util.ts +289 -0
  54. package/src/model.util.ts +2211 -0
  55. package/src/permission.util.ts +136 -0
  56. package/src/route.util.ts +12 -0
  57. package/src/storage.util.ts +102 -0
  58. package/src/validation.util.ts +338 -0
  59. package/tsconfig.json +23 -0
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./auth.util";
2
+ export * from "./controller.util";
3
+ export * from "./conversion.util";
4
+ export * from "./context.util";
5
+ export * from "./db.util";
6
+ export * from "./middleware.util";
7
+ export * from "./model.util";
8
+ export * from "./permission.util";
9
+ export * from "./route.util";
10
+ export * from "./storage.util";
11
+ export * from "./validation.util";
12
+ export * from "./mail.util";
13
+ export * from "./logger.util";
@@ -0,0 +1,170 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+
5
+
6
+ type LogType = "start" | "info" | "error" | "warning" | "cron" | "queue" | "queueError" | "cronError" | "socket" | "socketError";
7
+
8
+ export interface AccessLog {
9
+ method : string
10
+ path : string
11
+ status : number
12
+ latency : number
13
+ ip ?: string | null
14
+ agent ?: string | null
15
+ at ?: string
16
+ }
17
+
18
+ export interface ErrorLog {
19
+ service ?: string
20
+ key ?: string
21
+ feature ?: string
22
+ error : string | null
23
+ reference ?: string | null
24
+ at ?: string
25
+ }
26
+
27
+
28
+
29
+
30
+ const colors: Record<LogType | "default", string> = {
31
+ default : "\x1b[0m", // default
32
+ start : "\x1b[32m", // green
33
+ info : "\x1b[36m", // cyan
34
+ error : "\x1b[31m", // red
35
+ warning : "\x1b[33m", // yellow
36
+ queue : "\x1b[34m", // blue
37
+ queueError : "\x1b[31m", // red
38
+ cron : "\x1b[35m", // magenta
39
+ cronError : "\x1b[31m", // red
40
+ socket : "\x1b[35m", // blue
41
+ socketError : "\x1b[31m", // red
42
+ };
43
+
44
+ const prefixes: Record<LogType, string> = {
45
+ start : "START",
46
+ info : "INFO",
47
+ error : "ERROR",
48
+ warning : "WARNING",
49
+ cron : "CRON",
50
+ queue : "QUEUE",
51
+ socket : "SOCKET",
52
+ queueError : "QUEUE ERROR",
53
+ cronError : "CRON ERROR",
54
+ socketError : "SOCKET ERROR",
55
+ };
56
+
57
+ function log(type: LogType, msg: string) {
58
+ const color = colors[type];
59
+ const prefix = prefixes[type];
60
+ // eslint-disable-next-line no-console
61
+ console.log(`${color}[${prefix}]${colors.default}`, msg);
62
+ }
63
+
64
+ export const logger = {
65
+ start : (msg: string) => log("start", msg),
66
+ info : (msg: string) => log("info", msg),
67
+ warning : (msg: string) => log("warning", msg),
68
+ queue : (msg: string) => log("queue", msg),
69
+ cron : (msg: string) => log("cron", msg),
70
+ socket : (msg: string) => log("socket", msg),
71
+
72
+ access : (msg: AccessLog) => logAccess(msg),
73
+
74
+ error: (msg: string, payload?: ErrorLog) => {
75
+ log("error", msg)
76
+ payload && logError({...payload, service: payload.service || 'app'})
77
+ },
78
+ queueError: (msg: string, payload?: ErrorLog) => {
79
+ log("queueError", msg)
80
+ payload && logError({...payload, service: payload.service || 'queue'})
81
+ },
82
+ cronError: (msg: string, payload?: ErrorLog) => {
83
+ log("cronError", msg)
84
+ payload && logError({...payload, service: payload.service || 'cron'})
85
+ },
86
+ socketError: (msg: string, payload?: ErrorLog) => {
87
+ log("socketError", msg)
88
+ payload && logError({...payload, service: payload.service || 'socket'})
89
+ },
90
+ };
91
+
92
+
93
+
94
+
95
+
96
+ type DriverName = "file" | "da"
97
+
98
+
99
+ const ACCESS_LOG_DRIVER = process.env.ACCESS_LOG_DRIVER || "file"
100
+ const ACCESS_LOG_LOG_DIR = process.env.ACCESS_LOG_DIR || "storage/logs/access"
101
+ const ACCESS_LOG_QUEUE = process.env.ACCESS_LOG_QUEUE || "access-log"
102
+
103
+
104
+ const ERROR_LOG_DRIVER = process.env.ERROR_LOG_DRIVER || "file"
105
+ const ERROR_LOG_LOG_DIR = process.env.ERROR_LOG_DIR || "storage/logs/error"
106
+ const ERROR_LOG_QUEUE = process.env.ERROR_LOG_QUEUE_PREFIX || "error-log"
107
+
108
+
109
+
110
+ // =====================
111
+ // ## Access Log Drivers
112
+ // =====================
113
+ const filePath = () => {
114
+ const d = new Date().toISOString().slice(0, 10)
115
+ return path.resolve( ACCESS_LOG_LOG_DIR, `access-${d}.log`)
116
+ }
117
+
118
+ const handlers: Record<DriverName, (log: AccessLog) => Promise<void>> = {
119
+ file: async (log) => {
120
+ const dir = path.resolve(ACCESS_LOG_LOG_DIR);
121
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
122
+
123
+ fs.appendFile(filePath(), JSON.stringify(log) + "\n", () => {})
124
+ },
125
+ da: async (log) => {
126
+ // try {
127
+ // await queue.add(ACCESS_LOG_QUEUE, log)
128
+ // } catch {}
129
+ }
130
+ }
131
+
132
+ const activeDrivers: DriverName[] = (ACCESS_LOG_DRIVER).split(",").map(v => v.trim()).filter((v): v is DriverName => v in handlers)
133
+
134
+ function logAccess(payload: AccessLog) {
135
+ for (const d of activeDrivers) {
136
+ handlers[d](payload)
137
+ }
138
+ }
139
+
140
+
141
+
142
+ // =====================
143
+ // ## Error Log Drivers
144
+ // =====================
145
+ const errorFilePath = () => {
146
+ const d = new Date().toISOString().slice(0, 10)
147
+ return path.resolve(ERROR_LOG_LOG_DIR, `error-${d}.log`)
148
+ }
149
+
150
+ const errorHandlers: Record<DriverName, (log: ErrorLog) => Promise<void>> = {
151
+ file: async (log) => {
152
+ const dir = path.resolve(ERROR_LOG_LOG_DIR);
153
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
154
+
155
+ fs.appendFile(errorFilePath(), JSON.stringify(log) + "\n", () => {});
156
+ },
157
+ da: async (log) => {
158
+ // try {
159
+ // await queue.add(ERROR_LOG_QUEUE, log)
160
+ // } catch {}
161
+ }
162
+ }
163
+
164
+ const activeErrorDrivers: DriverName[] = ERROR_LOG_DRIVER.split(",").map(v => v.trim()).filter((v): v is DriverName => v in errorHandlers)
165
+
166
+ function logError(payload: ErrorLog) {
167
+ for (const d of activeErrorDrivers) {
168
+ errorHandlers[d](payload)
169
+ }
170
+ }
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import nodemailer, { SentMessageInfo } from "nodemailer";
4
+ import { logger } from "@utils";
5
+
6
+
7
+
8
+ export interface SendMailOptions {
9
+ to : string;
10
+ subject : string;
11
+ content ?: string;
12
+ text ?: string;
13
+ attachments ?: {
14
+ filename : string;
15
+ path : string;
16
+ }[];
17
+ }
18
+
19
+
20
+
21
+ // =============================>
22
+ // ## Mail: Send mail
23
+ // =============================>
24
+ export async function sendMail(options: {
25
+ to : string;
26
+ subject : string;
27
+ text ?: string;
28
+ content ?: string;
29
+ attachments ?: { filename: string; path: string }[];
30
+ }) {
31
+ const transporter = nodemailer.createTransport({
32
+ host : process.env.MAIL_HOST,
33
+ port : Number(process.env.MAIL_PORT),
34
+ secure : Number(process.env.MAIL_PORT) === 465,
35
+ auth : {
36
+ user : process.env.MAIL_USERNAME,
37
+ pass : process.env.MAIL_PASSWORD,
38
+ },
39
+ });
40
+
41
+ const info = (await transporter.sendMail({
42
+ from : `${process.env.MAIL_FROM_NAME || process.env.APP_NAME} <${process.env.MAIL_FROM_ADDRESS || process.env.MAIL_USERNAME}>`,
43
+ to : options.to,
44
+ subject : options.subject,
45
+ text : options.text,
46
+ html : options.content,
47
+ attachments : options.attachments,
48
+ })) as SentMessageInfo;
49
+
50
+ logger.info(`Email sent successfully: ${info.messageId}`)
51
+ return info;
52
+ }
53
+
54
+
55
+
56
+ // =============================>
57
+ // ## Mail: Render mail template
58
+ // =============================>
59
+ export function renderMailTemplate(template: string, options: Record<string, string>) {
60
+ const templateDir = join(import.meta.dir, "./../outputs/mails/templates");
61
+
62
+ const contentPath = join(templateDir, `${template}.mail.stub`);
63
+ let content = readFileSync(contentPath, "utf-8");
64
+
65
+ for (const [key, value] of Object.entries(options)) {
66
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
67
+ content = content.replace(regex, value);
68
+ }
69
+
70
+ let layout = readFileSync(join(templateDir, "layout.mail.stub"), "utf-8");
71
+
72
+ const globalVars = {
73
+ ...options,
74
+ date : "20-10-2025",
75
+ app_name : process.env.APP_NAME || "",
76
+ };
77
+
78
+ for (const [key, value] of Object.entries(globalVars)) {
79
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
80
+ layout = layout.replace(regex, value);
81
+ }
82
+
83
+ layout = layout.replace("{{content}}", content);
84
+
85
+ return layout;
86
+ }
@@ -0,0 +1,289 @@
1
+ import { Elysia, status } from 'elysia'
2
+ import { auth, context, logger } from '@utils'
3
+
4
+ declare module "elysia" {
5
+ interface Elysia {
6
+ api(
7
+ basePath: string,
8
+ controller: {
9
+ index ?: any
10
+ store ?: any
11
+ show ?: any
12
+ update ?: any
13
+ destroy ?: any
14
+ }
15
+ ): this
16
+ }
17
+ }
18
+
19
+
20
+ const errors = {
21
+ unauthorized: {
22
+ status: 401,
23
+ message: "Unauthorized!"
24
+ },
25
+ ratelimited: {
26
+ status: 429,
27
+ message: "Too many requests!"
28
+ },
29
+ notfound: {
30
+ status: 404,
31
+ message: "Endpoint not found!"
32
+ },
33
+ request: {
34
+ status: 400,
35
+ message: "Bad Request!"
36
+ },
37
+ error: {
38
+ status: 500,
39
+ message: "Endpoint not found!"
40
+ }
41
+ }
42
+
43
+
44
+ export const middleware = {
45
+ // =============================>
46
+ // ## Middleware: Auth hand;er
47
+ // =============================>
48
+ Auth: (app: Elysia) => app.derive(async ({ request }) => {
49
+ const authHeader = request.headers.get('authorization')
50
+
51
+ if (!authHeader || !authHeader.startsWith('Bearer ')) return { user: null, permissions: [], token: null }
52
+
53
+ const bearer = authHeader.substring(7).trim()
54
+ const result = await auth.verifyAccessToken(bearer, request)
55
+
56
+ if (!result) return { user: null, permissions: [], token: null };
57
+
58
+ return {
59
+ user: result.user,
60
+ permissions: result.permissions,
61
+ token: result.token,
62
+ }
63
+ }),
64
+
65
+
66
+ // =============================>
67
+ // ## Middleware: Private handler
68
+ // =============================>
69
+ Private: (app: Elysia) => app.derive(async ({ user }: Record<string, any> | any) => {
70
+ if (!user) {
71
+ throw status(errors.unauthorized.status, { message: errors.unauthorized.message })
72
+ }
73
+ }),
74
+
75
+
76
+ // =============================>
77
+ // ## Middleware: Cors handler
78
+ // =============================>
79
+ Cors: (app: Elysia) => app.onRequest(({ request, set }) => {
80
+ const origin = request.headers.get('origin') ?? ''
81
+ let allowedOrigin: string = '*'
82
+
83
+ const originsConf = process.env.APP_CORS_ORIGINS || '*'
84
+
85
+ if (originsConf !== '*') {
86
+ try {
87
+ const allowedOrigins = JSON.parse(originsConf)
88
+ if (Array.isArray(allowedOrigins) && allowedOrigins.includes(origin)) {
89
+ allowedOrigin = origin || ""
90
+ }
91
+ } catch (e) {
92
+ const em = 'Cors Error: Failed to parse APP_CORS_ORIGINS, fallback to "*"'
93
+ logger.error(em, { error: em })
94
+ allowedOrigin = ''
95
+ }
96
+ }
97
+
98
+ set.headers['Access-Control-Allow-Origin'] = allowedOrigin
99
+ set.headers['Access-Control-Allow-Methods'] = process.env.APP_CORS_METHODS || 'GET, POST, PUT, DELETE, OPTIONS'
100
+ set.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Option, x-App'
101
+ set.headers['Access-Control-Allow-Credentials'] = 'true'
102
+
103
+ if (request.method === 'OPTIONS') {
104
+ return new Response(null, { status: 204, })
105
+ }
106
+ }),
107
+
108
+
109
+ // =============================>
110
+ // ## Middleware: Rate limiter handler
111
+ // =============================>
112
+ RateLimiter: (app: Elysia, options?: { windowMs?: number, max?: number }) => app.onRequest(({ request, set, store }) => {
113
+ const max = options?.max || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATE_LIMIT) : 60 )
114
+ const windowMs = options?.windowMs || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATELIMIT_COUNTDOWN) : 60_000 )
115
+
116
+ const user = (store as any)?.user
117
+ const key = getClientKey(request, user?.id)
118
+
119
+ const now = Date.now()
120
+ let record = rateLimitStore.get(key)
121
+
122
+ if (!record || record.expiresAt < now) {
123
+ record = { count: 1, expiresAt: now + windowMs }
124
+ rateLimitStore.set(key, record)
125
+ } else {
126
+ record.count++
127
+ }
128
+
129
+ set.headers['X-RateLimit-Limit'] = String(max)
130
+ set.headers['X-RateLimit-Remaining'] = String(Math.max(0, max - record.count))
131
+ set.headers['X-RateLimit-Reset'] = String(record.expiresAt)
132
+
133
+ if (record.count > max) throw status(errors.ratelimited.status, { message: errors.ratelimited.message });
134
+ }),
135
+
136
+
137
+ // =============================>
138
+ // ## Middleware: Body parse handler
139
+ // =============================>
140
+ BodyParse: (app: Elysia) => app.state<{ rawBody?: any }>({}).onRequest(async ({ request, store }) => {
141
+ const text = await request.clone().text();
142
+
143
+ const contentType = request.headers.get("content-type") || "";
144
+ let rawBody: any = {};
145
+
146
+ try {
147
+ if (contentType.includes("application/json")) {
148
+ rawBody = text ? JSON.parse(text) : {};
149
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
150
+ const params = new URLSearchParams(text);
151
+ for (const [key, value] of params.entries()) bodyParseNestedSet(rawBody, key, value);
152
+ } else if (contentType.includes("multipart/form-data")) {
153
+ const formData = await request.clone().formData();
154
+ for (const [key, value] of formData.entries()) bodyParseNestedSet(rawBody, key, value);
155
+ } else {
156
+ rawBody = {};
157
+ }
158
+ } catch (e) {
159
+ const em = e instanceof Error ? e.message : String(e)
160
+ logger.error(`Body parse error: ${em}`, { error: em })
161
+ rawBody = {};
162
+ throw status(errors.request.status, { message: errors.request.message })
163
+ }
164
+
165
+ store.rawBody = rawBody;
166
+ }).derive(({ store }) => {
167
+ const payload = bodyParseKeyFormat(store.rawBody || {});
168
+ return { payload };
169
+ }),
170
+
171
+
172
+ AccessLog: (app: Elysia) => app.state<{ startedAt?: number }>({}).onRequest(({ store }) => { store.startedAt = Date.now() }).onAfterResponse(({ request, set, store }) => {
173
+ const method = request.method
174
+ const url = new URL(request.url)
175
+ const path = url.pathname
176
+ const status = Number(set.status) ?? 200
177
+ const latency = Date.now() - (store.startedAt ?? Date.now())
178
+ const agent = request.headers.get("user-agent") || 'unknown'
179
+ const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
180
+
181
+ logger.info(`${method} : ${path} - ${status} - ${latency}ms - ${ip}]`)
182
+ logger.access({ method, path, status, latency, ip, agent })
183
+ }),
184
+
185
+
186
+ // =============================>
187
+ // ## Middleware: Error handler
188
+ // =============================>
189
+ ErrorHandler: (app: Elysia) => app.onError(({ code, set, error, request }) => {
190
+ if (code === 'NOT_FOUND') {
191
+ set.status = errors.notfound.status
192
+ return { message: errors.notfound.message }
193
+ }
194
+
195
+ if (code === 'INTERNAL_SERVER_ERROR') {
196
+ set.status = errors.error.status
197
+ const em = error.message
198
+ const url = new URL(request.url)
199
+ const path = url.pathname
200
+
201
+ logger.error(`error: ${em}`, { error: em, reference: path })
202
+ return { message: em }
203
+ }
204
+ }),
205
+
206
+ Context: (app: Elysia) => app.derive(async ({ store }) => {
207
+ const userId = (store as any)?.user?.id
208
+
209
+ return context.run({
210
+ user_id: userId,
211
+ },() => ({})
212
+ )
213
+ }),
214
+ }
215
+
216
+
217
+
218
+ // =============================>
219
+ // ## Middleware: Body parse helpers
220
+ // =============================>
221
+ function bodyParseKeyFormat(input: any): any {
222
+ if ( typeof input !== "object" || input === null || input instanceof File ) return input;
223
+
224
+ if (Array.isArray(input)) return input.map(bodyParseKeyFormat)
225
+
226
+ const result: any = {}
227
+ for (const [key, value] of Object.entries(input)) {
228
+ if (key.includes(".") || key.includes("[")) {
229
+ bodyParseNestedSet(result, key, bodyParseKeyFormat(value))
230
+ } else {
231
+ result[key] = bodyParseKeyFormat(value)
232
+ }
233
+ }
234
+ return result
235
+ }
236
+
237
+
238
+ function bodyParseNestedSet(obj: any, path: string, value: any) {
239
+ const parts = bodyParsePathFormat(path);
240
+ let current = obj;
241
+
242
+ for (let i = 0; i < parts.length; i++) {
243
+ const key = parts[i];
244
+ const isLast = i === parts.length - 1;
245
+
246
+ if (isLast) {
247
+ current[key] = bodyParseValueFormat(value);
248
+ } else {
249
+ if (!(key in current)) {
250
+ const nextKey = parts[i + 1];
251
+ current[key] = isNaN(Number(nextKey)) ? {} : [];
252
+ }
253
+ current = current[key];
254
+ }
255
+ }
256
+ }
257
+
258
+ function bodyParsePathFormat(path: string): string[] {
259
+ return path.replace(/\[(\w+)\]/g, ".$1").replace(/^\./, "").split(".");
260
+ }
261
+
262
+ function bodyParseValueFormat(value: any) {
263
+ if (value == "" || value == null || value == "null") return null;
264
+ if (typeof value !== "string") return value;
265
+ if (value === "true") return true;
266
+ if (value === "false") return false;
267
+ if (!isNaN(Number(value))) return Number(value);
268
+ return value;
269
+ }
270
+
271
+
272
+
273
+ // =============================>
274
+ // ## Middleware: Rate Limiter Helpers
275
+ // =============================>
276
+ type RateLimitRecord = {
277
+ count: number
278
+ expiresAt: number
279
+ }
280
+
281
+ const rateLimitStore = new Map<string, RateLimitRecord>()
282
+
283
+ function getClientKey(request: Request, userId?: string | number) {
284
+ if (userId) return `user:${userId}`
285
+
286
+ const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
287
+
288
+ return `ip:${ip}`
289
+ }