@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
@@ -0,0 +1,136 @@
1
+ import { ControllerContext } from "elysia"
2
+
3
+ type KeyDigit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
4
+ export type KeyFeature = `${"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"}${KeyDigit}${KeyDigit}`
5
+ export type KeyAccess = `${KeyDigit}${KeyDigit}`
6
+ export type KeyPermission = `${KeyFeature}.${KeyAccess}` | KeyAccess
7
+
8
+ const features = new Map<KeyFeature, { key: KeyFeature; name: string }>()
9
+ const accesses = new Map<KeyAccess, any>()
10
+
11
+ type FeatureAccess = Partial<Record<KeyFeature, {
12
+ name: string
13
+ accesses: Partial<Record<KeyAccess, string>>
14
+ }>>
15
+
16
+ export const permission = {
17
+ register: (def: FeatureAccess) => {
18
+ const featureAccessMap = new Map<string, string>()
19
+ let defaultFeature: KeyFeature | null = null
20
+
21
+ for (const [featureKey, feature] of Object.entries(def)) {
22
+ if (!defaultFeature) defaultFeature = featureKey as KeyFeature
23
+
24
+ registerFeature({
25
+ key: featureKey as KeyFeature,
26
+ name: feature.name
27
+ })
28
+
29
+ for (const [accessKey, accessName] of Object.entries(feature.accesses)) {
30
+ const permKey =
31
+ `${featureKey}.${String(accessKey).padStart(2, "0")}`
32
+
33
+ registerAccess({
34
+ featureKey,
35
+ accessKey,
36
+ accessName,
37
+ permKey
38
+ })
39
+
40
+ featureAccessMap.set(
41
+ `${featureKey}.${accessKey}`,
42
+ permKey
43
+ )
44
+ }
45
+ }
46
+
47
+ return createScopeApi(defaultFeature!)
48
+ },
49
+
50
+ getFeatures: () => [...features.values()],
51
+
52
+ getAccesses: () => {
53
+ const result: Record<string, {
54
+ key: string
55
+ name: string
56
+ accesses: { key: string; name: string }[]
57
+ }> = {}
58
+
59
+ for (const feature of features.values()) {
60
+ result[String(feature.key)] = {
61
+ key: String(feature.key),
62
+ name: feature.name,
63
+ accesses: []
64
+ }
65
+ }
66
+
67
+ for (const access of accesses.values()) {
68
+ const featureKey = String(access.featureKey)
69
+
70
+ if (!result[featureKey]) continue
71
+
72
+ result[featureKey].accesses.push({
73
+ key: String(access.accessKey).padStart(2, "0"),
74
+ name: access.accessName
75
+ })
76
+ }
77
+
78
+ return Object.values(result)
79
+ },
80
+ }
81
+
82
+
83
+ function normalize(
84
+ raw: KeyPermission,
85
+ defaultFeature?: KeyFeature
86
+ ): KeyPermission {
87
+ if (!raw.includes(".") && defaultFeature) {
88
+ return `${defaultFeature}.${String(raw).padStart(2, "0") as KeyAccess}`
89
+ }
90
+
91
+ return raw
92
+ }
93
+
94
+ function registerFeature(f: { key: KeyFeature; name: string }) {
95
+ if (!features.has(f.key)) {
96
+ features.set(f.key, f)
97
+ }
98
+ }
99
+
100
+ function registerAccess(a: any) {
101
+ if (!accesses.has(a.permKey)) {
102
+ accesses.set(a.permKey, a)
103
+ }
104
+ }
105
+
106
+ function createPermission(keys: KeyPermission[]) {
107
+ return {
108
+ keys,
109
+
110
+ orHave(raw: KeyPermission) {
111
+ return createPermission([
112
+ ...this.keys,
113
+ normalize(raw) as KeyPermission
114
+ ])
115
+ },
116
+
117
+ guard(c: ControllerContext) {
118
+ const permissions = new Set(c.permissions || [])
119
+
120
+ const ok = this.keys.some(k => permissions?.has(k))
121
+ if (!ok) {
122
+ c.responseForbidden()
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ export function createScopeApi(defaultFeature: KeyFeature) {
129
+ return {
130
+ have(raw: KeyPermission) {
131
+ const key = normalize(raw, defaultFeature)
132
+ return createPermission([key])
133
+ }
134
+ }
135
+ }
136
+
@@ -0,0 +1,12 @@
1
+ // ================================>
2
+ // ## Route: Basic api routers
3
+ // ================================>
4
+ export function api(app: any, basePath: string, controller: any) {
5
+ return app.group(basePath, (group: any) => group
6
+ .get("/", controller.index)
7
+ .post("/", controller.store)
8
+ .get("/:id", controller.show)
9
+ .put("/:id", controller.update)
10
+ .delete("/:id", controller.destroy)
11
+ )
12
+ }
@@ -0,0 +1,102 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Elysia } from "elysia";
4
+ import { db } from "@utils/db.util";
5
+
6
+
7
+
8
+ // ================================>
9
+ // ## Storage: Middleware storage handler
10
+ // ================================>
11
+ export const storage = (app: Elysia) => app.get("/storage/*", async ({ params, set, user }: Record<string, any>) => {
12
+ const requestedPath = params["*"];
13
+ const baseDir = path.resolve("storage", "public");
14
+ const targetPath = path.resolve(baseDir, requestedPath);
15
+
16
+ if (!targetPath.startsWith(baseDir)) {
17
+ set.status = 400;
18
+ return { error: "Invalid path" };
19
+ }
20
+
21
+ if (fs.existsSync(targetPath)) {
22
+ const ext = path.extname(targetPath).toLowerCase();
23
+ const mimeTypes: Record<string, string> = {
24
+ ".jpg" : "image/jpeg",
25
+ ".jpeg" : "image/jpeg",
26
+ ".png" : "image/png",
27
+ ".webp" : "image/webp",
28
+ ".gif" : "image/gif",
29
+ ".pdf" : "application/pdf",
30
+ ".txt" : "text/plain",
31
+ ".json" : "application/json",
32
+ ".svg" : "image/svg+xml",
33
+ };
34
+
35
+ const buffer = fs.readFileSync(targetPath);
36
+
37
+ set.headers["Content-Type"] = mimeTypes[ext] || "application/octet-stream";
38
+ set.headers["Content-Length"] = buffer.length.toString();
39
+
40
+ return new Response(buffer);
41
+ } else {
42
+ const baseDir = path.resolve("storage", "private");
43
+ const targetPath = path.resolve(baseDir, requestedPath);
44
+
45
+ if (fs.existsSync(targetPath)) {
46
+ if (!user) {
47
+ set.status = 404
48
+ return { error: "File not found" };
49
+ }
50
+
51
+ const file = await db("storages").where({ path: requestedPath, disk: "private" }).first()
52
+
53
+ if (!file) {
54
+ set.status = 404
55
+ return { error: "File not found" }
56
+ }
57
+
58
+ let hasAccess = file.user_id === user.id
59
+
60
+ if (!hasAccess) {
61
+ hasAccess = await db("storage_permissions").where("storage_id", file.id)
62
+ .andWhere((q) => {
63
+ q.where("user_id", user.id)
64
+ .orWhere("role_id", user.role_id)
65
+ })
66
+ .first().then(Boolean)
67
+ }
68
+
69
+
70
+ if (!hasAccess) {
71
+ set.status = 404
72
+ return { error: "File not found" }
73
+ }
74
+
75
+ } else {
76
+ set.status = 404;
77
+ return { error: "File not found" };
78
+ }
79
+
80
+ const ext = path.extname(targetPath).toLowerCase();
81
+ const mimeTypes: Record<string, string> = {
82
+ ".jpg" : "image/jpeg",
83
+ ".jpeg" : "image/jpeg",
84
+ ".png" : "image/png",
85
+ ".webp" : "image/webp",
86
+ ".gif" : "image/gif",
87
+ ".pdf" : "application/pdf",
88
+ ".txt" : "text/plain",
89
+ ".json" : "application/json",
90
+ ".svg" : "image/svg+xml",
91
+ };
92
+
93
+ const buffer = fs.readFileSync(targetPath);
94
+
95
+ set.headers["Content-Type"] = mimeTypes[ext] || "application/octet-stream";
96
+ set.headers["Content-Length"] = buffer.length.toString();
97
+
98
+ return new Response(buffer);
99
+ }
100
+
101
+
102
+ });
@@ -0,0 +1,338 @@
1
+ import validator from "validator"
2
+ import { db } from "@utils"
3
+
4
+ // ==========================>
5
+ // ## Validation: Rules of validation
6
+ // ==========================>
7
+
8
+ export type ValidationRule =
9
+ | "required"
10
+ | "string"
11
+ | "numeric"
12
+ | "number"
13
+ | "boolean"
14
+ | "email"
15
+ | "url"
16
+ | "date"
17
+ | "confirmed"
18
+ | "array"
19
+ | `min:`
20
+ | `min:${number}`
21
+ | `max:`
22
+ | `max:${number}`
23
+ | `between:`
24
+ | `between:${number},${number}`
25
+ | `in:`
26
+ | `in:${string}`
27
+ | `not_in:`
28
+ | `not_in:${string}`
29
+ | `same:`
30
+ | `same:${string}`
31
+ | `different:`
32
+ | `different:${string}`
33
+ | `regex:`
34
+ | `regex:${string}`
35
+ | `unique:`
36
+ | `unique:${string},${string}`
37
+ | `exists:`
38
+ | `exists:${string},${string}`
39
+
40
+ export type ValidationRules = Record<string, ValidationRule[] | string>
41
+
42
+ export interface ValidationResult {
43
+ valid : boolean
44
+ errors : Record<string, string[]>
45
+ }
46
+
47
+ // ==================================>
48
+ // ## Check validate field from rules
49
+ // ==================================>
50
+ export async function validate(
51
+ data: Record<string, any>,
52
+ rules: ValidationRules
53
+ ): Promise<ValidationResult> {
54
+ const errors: Record<string, string[]> = {}
55
+
56
+ for (const field in rules) {
57
+ const fieldRules = normalizeRules(rules[field])
58
+
59
+ if (field.includes("*")) {
60
+ // const [arrayPath, childPath] = field.split(".*.")
61
+ // const arr = getNestedValue(data, arrayPath)
62
+
63
+ // if (!Array.isArray(arr)) {
64
+ // addError(errors, arrayPath, `${arrayPath} harus berupa array`)
65
+ // continue
66
+ // }
67
+
68
+ // for (let i = 0; i < arr.length; i++) {
69
+ // const value = childPath
70
+ // ? getNestedValue(arr[i], childPath)
71
+ // : arr[i]
72
+
73
+ // const itemField = childPath
74
+ // ? `${arrayPath}.${i}.${childPath}`
75
+ // : `${arrayPath}.${i}`
76
+
77
+ // await checkRules({ field: itemField, value, rules: fieldRules, data, errors })
78
+ // }
79
+ const segments = field.split(".")
80
+
81
+ await nestedValidation({ value: data, segments, rules: fieldRules, fieldPath: "", data, errors })
82
+
83
+ continue
84
+ }
85
+
86
+
87
+ const value = getNestedValue(data, field) ?? ""
88
+
89
+ await checkRules({ field, value, rules: fieldRules, data, errors })
90
+ }
91
+
92
+ return {
93
+ valid: Object.keys(errors).length === 0,
94
+ errors
95
+ }
96
+ }
97
+
98
+
99
+ async function checkRules({ field, value, rules, data, errors } : { field: string, value: any, rules: ValidationRule[], data: any, errors: Record<string, string[]> }) {
100
+ for (const rule of rules) {
101
+ const [name, param] = rule.split(":") as [string, string | undefined]
102
+
103
+ switch (name) {
104
+ // === BASIC ===
105
+ case "required":
106
+ if (validator.isEmpty(String(value).trim())) {
107
+ addError(errors, field, `${field} wajib diisi`)
108
+ }
109
+ break
110
+
111
+ case "string":
112
+ case "text":
113
+ if (typeof value !== "string") {
114
+ addError(errors, field, `${field} harus berupa string`)
115
+ }
116
+ break
117
+
118
+ case "numeric":
119
+ case "number":
120
+ if (!validator.isNumeric(String(value))) {
121
+ addError(errors, field, `${field} harus berupa angka`)
122
+ }
123
+ break
124
+
125
+ case "boolean":
126
+ if (!(value === true || value === false || value === "true" || value === "false" || value === 1 || value === 0)) {
127
+ addError(errors, field, `${field} harus berupa boolean`)
128
+ }
129
+ break
130
+
131
+ case "email":
132
+ if (!validator.isEmail(String(value))) {
133
+ addError(errors, field, `${field} harus berupa email yang valid`)
134
+ }
135
+ break
136
+
137
+ case "url":
138
+ if (!validator.isURL(String(value))) {
139
+ addError(errors, field, `${field} harus berupa URL yang valid`)
140
+ }
141
+ break
142
+
143
+ case "date":
144
+ if (!validator.isDate(String(value))) {
145
+ addError(errors, field, `${field} harus berupa tanggal yang valid`)
146
+ }
147
+ break
148
+
149
+ // === LENGTH ===
150
+ case "min": {
151
+ const min = parseInt(param!)
152
+ if (!validator.isLength(String(value), { min })) {
153
+ addError(errors, field, `${field} minimal ${min} karakter`)
154
+ }
155
+ break
156
+ }
157
+
158
+ case "max": {
159
+ const max = parseInt(param!)
160
+ if (!validator.isLength(String(value), { max })) {
161
+ addError(errors, field, `${field} maksimal ${max} karakter`)
162
+ }
163
+ break
164
+ }
165
+
166
+ case "between": {
167
+ const [minVal, maxVal] = param!.split(",").map(Number)
168
+ if (!validator.isLength(String(value), { min: minVal, max: maxVal })) {
169
+ addError(errors, field, `${field} harus antara ${minVal} - ${maxVal} karakter`)
170
+ }
171
+ break
172
+ }
173
+
174
+ // === SET MEMBERSHIP ===
175
+ case "in": {
176
+ const allowed = param!.split(",")
177
+ if (!allowed.includes(String(value))) {
178
+ addError(errors, field, `${field} harus salah satu dari: ${allowed.join(", ")}`)
179
+ }
180
+ break
181
+ }
182
+
183
+ case "not_in": {
184
+ const notAllowed = param!.split(",")
185
+ if (notAllowed.includes(String(value))) {
186
+ addError(errors, field, `${field} tidak boleh salah satu dari: ${notAllowed.join(", ")}`)
187
+ }
188
+ break
189
+ }
190
+
191
+ case "array":
192
+ if (!Array.isArray(value)) {
193
+ addError(errors, field, `${field} harus berupa array`)
194
+ }
195
+ break
196
+
197
+ // === RELATIONAL ===
198
+ case "confirmed":
199
+ if (value !== getNestedValue(data, `${field}_confirmation`)) {
200
+ addError(errors, field, `${field} tidak sama dengan konfirmasi`)
201
+ }
202
+ break
203
+
204
+ case "same":
205
+ if (value !== getNestedValue(data, param!)) {
206
+ addError(errors, field, `${field} harus sama dengan ${param}`)
207
+ }
208
+ break
209
+
210
+ case "different":
211
+ if (value === getNestedValue(data, param!)) {
212
+ addError(errors, field, `${field} harus berbeda dengan ${param}`)
213
+ }
214
+ break
215
+
216
+ // === REGEX ===
217
+ case "regex":
218
+ try {
219
+ const pattern = new RegExp(param!)
220
+ if (!pattern.test(String(value))) {
221
+ addError(errors, field, `${field} tidak sesuai format`)
222
+ }
223
+ } catch {
224
+ addError(errors, field, `Regex rule untuk ${field} tidak valid`)
225
+ }
226
+ break
227
+
228
+ // === DATABASE VALIDATION ===
229
+ case "unique": {
230
+ const [table, column, exceptId] = param!.split(",")
231
+ const query = db.table(table).where(column, value)
232
+ if (exceptId) query.whereNot("id", exceptId)
233
+ const existing = await query.first()
234
+ if (existing) {
235
+ addError(errors, field, `${field} sudah digunakan`)
236
+ }
237
+ break
238
+ }
239
+
240
+ case "exists": {
241
+ const [table, column] = param!.split(",")
242
+ const existing = await db.table(table).where(column, value).first()
243
+ if (!existing) {
244
+ addError(errors, field, `${field} tidak ditemukan di ${table}`)
245
+ }
246
+ break
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+
253
+
254
+ async function nestedValidation({
255
+ value,
256
+ segments,
257
+ rules,
258
+ fieldPath,
259
+ data,
260
+ errors
261
+ }: {
262
+ value: any
263
+ segments: string[]
264
+ rules: ValidationRule[]
265
+ fieldPath: string
266
+ data: any
267
+ errors: Record<string, string[]>
268
+ }) {
269
+ if (segments.length === 0) {
270
+ await checkRules({
271
+ field: fieldPath,
272
+ value,
273
+ rules,
274
+ data,
275
+ errors
276
+ })
277
+ return
278
+ }
279
+
280
+ const [segment, ...rest] = segments
281
+
282
+ if (segment === "*") {
283
+ if (!Array.isArray(value)) {
284
+ addError(errors, fieldPath, `${fieldPath} harus berupa array`)
285
+ return
286
+ }
287
+
288
+ for (let i = 0; i < value.length; i++) {
289
+ await nestedValidation({
290
+ value: value[i],
291
+ segments: rest,
292
+ rules,
293
+ fieldPath: `${fieldPath}.${i}`,
294
+ data,
295
+ errors
296
+ })
297
+ }
298
+ } else {
299
+ await nestedValidation({
300
+ value: value?.[segment],
301
+ segments: rest,
302
+ rules,
303
+ fieldPath: fieldPath ? `${fieldPath}.${segment}` : segment,
304
+ data,
305
+ errors
306
+ })
307
+ }
308
+ }
309
+
310
+
311
+
312
+ // ==================================>
313
+ // ## Validation helpers
314
+ // ==================================>
315
+ function getNestedValue(obj: any, path: string): any {
316
+ if (!obj || typeof obj !== "object") return undefined
317
+
318
+ const normalizedPath = path
319
+ .replace(/\[(\w+)\]/g, '.$1')
320
+ .replace(/\['([^']+)'\]/g, '.$1')
321
+ .replace(/\["([^"]+)"\]/g, '.$1')
322
+
323
+ return normalizedPath.split('.').reduce((acc, key) => {
324
+ if (acc && Object.prototype.hasOwnProperty.call(acc, key)) {
325
+ return acc[key]
326
+ }
327
+ return undefined
328
+ }, obj)
329
+ }
330
+
331
+ function normalizeRules(rules: ValidationRule[] | string): ValidationRule[] {
332
+ if (Array.isArray(rules)) return rules
333
+ return rules.split("|") as ValidationRule[]
334
+ }
335
+
336
+ function addError(errors: Record<string, string[]>, field: string, message: string) {
337
+ errors[field] = [...(errors[field] || []), message]
338
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "ignoreDeprecations": "6.0",
4
+ "target": "ES2021",
5
+ "module": "ES2022",
6
+ "moduleResolution": "node",
7
+ "declaration": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "baseUrl": ".",
16
+ "types": ["bun-types", "elysia"],
17
+ "paths": {
18
+ "@utils": ["src/index.ts"],
19
+ "@utils/*": ["src/*"]
20
+ }
21
+ },
22
+ "include": ["src/**/*"]
23
+ }