@skalfa/skalfa-api-core 1.0.3 → 1.0.8

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 (284) hide show
  1. package/CONTRIBUTING.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +60 -0
  4. package/dist/auth/auth.d.ts +18 -15
  5. package/dist/auth/auth.js +20 -203
  6. package/dist/auth/auth.js.map +1 -1
  7. package/dist/auth/create-access-token.d.ts +4 -0
  8. package/dist/auth/create-access-token.js +26 -0
  9. package/dist/auth/create-access-token.js.map +1 -0
  10. package/dist/auth/create-user-mail-token.d.ts +4 -0
  11. package/dist/auth/create-user-mail-token.js +19 -0
  12. package/dist/auth/create-user-mail-token.js.map +1 -0
  13. package/dist/auth/helpers/generate-agent-id.d.ts +1 -0
  14. package/dist/auth/helpers/generate-agent-id.js +7 -0
  15. package/dist/auth/helpers/generate-agent-id.js.map +1 -0
  16. package/dist/auth/helpers/get-request-ip.d.ts +1 -0
  17. package/dist/auth/helpers/get-request-ip.js +4 -0
  18. package/dist/auth/helpers/get-request-ip.js.map +1 -0
  19. package/dist/auth/helpers/get-user-permissions.d.ts +1 -0
  20. package/dist/auth/helpers/get-user-permissions.js +9 -0
  21. package/dist/auth/helpers/get-user-permissions.js.map +1 -0
  22. package/dist/auth/helpers/index.d.ts +3 -0
  23. package/dist/auth/helpers/index.js +4 -0
  24. package/dist/auth/helpers/index.js.map +1 -0
  25. package/dist/auth/list-user-sessions.d.ts +1 -0
  26. package/dist/auth/list-user-sessions.js +10 -0
  27. package/dist/auth/list-user-sessions.js.map +1 -0
  28. package/dist/auth/revalidate-user-permissions-by-role.d.ts +1 -0
  29. package/dist/auth/revalidate-user-permissions-by-role.js +12 -0
  30. package/dist/auth/revalidate-user-permissions-by-role.js.map +1 -0
  31. package/dist/auth/revalidate-user-permissions.d.ts +1 -0
  32. package/dist/auth/revalidate-user-permissions.js +21 -0
  33. package/dist/auth/revalidate-user-permissions.js.map +1 -0
  34. package/dist/auth/revoke-access-token.d.ts +1 -0
  35. package/dist/auth/revoke-access-token.js +5 -0
  36. package/dist/auth/revoke-access-token.js.map +1 -0
  37. package/dist/auth/verify-access-token.d.ts +1 -0
  38. package/dist/auth/verify-access-token.js +47 -0
  39. package/dist/auth/verify-access-token.js.map +1 -0
  40. package/dist/auth/verify-user-mail-token.d.ts +1 -0
  41. package/dist/auth/verify-user-mail-token.js +21 -0
  42. package/dist/auth/verify-user-mail-token.js.map +1 -0
  43. package/dist/commands/cli.js +18 -27
  44. package/dist/commands/cli.js.map +1 -1
  45. package/dist/commands/make/basic-controller.js +1 -1
  46. package/dist/commands/make/basic-controller.js.map +1 -1
  47. package/dist/commands/make/basic-migration.d.ts +1 -1
  48. package/dist/commands/make/basic-migration.js +2 -2
  49. package/dist/commands/make/basic-migration.js.map +1 -1
  50. package/dist/commands/make/basic-model.js +1 -1
  51. package/dist/commands/make/basic-model.js.map +1 -1
  52. package/dist/commands/make/basic-seeder.js +1 -1
  53. package/dist/commands/make/basic-seeder.js.map +1 -1
  54. package/dist/commands/make/blueprint.js +1 -1
  55. package/dist/commands/make/blueprint.js.map +1 -1
  56. package/dist/commands/make/da-migration.js +3 -3
  57. package/dist/commands/make/da-migration.js.map +1 -1
  58. package/dist/commands/make/mail.js +2 -2
  59. package/dist/commands/make/mail.js.map +1 -1
  60. package/dist/commands/make/notification.js +1 -1
  61. package/dist/commands/make/notification.js.map +1 -1
  62. package/dist/commands/make/queue.js +1 -1
  63. package/dist/commands/make/queue.js.map +1 -1
  64. package/dist/commands/make/resource.d.ts +2 -0
  65. package/dist/commands/make/resource.js +19 -0
  66. package/dist/commands/make/resource.js.map +1 -0
  67. package/dist/commands/make/skalfa-controller.d.ts +3 -0
  68. package/dist/commands/make/{light-controller.js → skalfa-controller.js} +9 -9
  69. package/dist/commands/make/skalfa-controller.js.map +1 -0
  70. package/dist/commands/make/skalfa-model.d.ts +3 -0
  71. package/dist/commands/make/{light-model.js → skalfa-model.js} +11 -11
  72. package/dist/commands/make/skalfa-model.js.map +1 -0
  73. package/dist/commands/runner/barrels.js.map +1 -1
  74. package/dist/commands/runner/blueprint/controller-generation.js +2 -2
  75. package/dist/commands/runner/blueprint/controller-generation.js.map +1 -1
  76. package/dist/commands/runner/blueprint/documentation-generation.js.map +1 -1
  77. package/dist/commands/runner/blueprint/migration-generation.js +3 -3
  78. package/dist/commands/runner/blueprint/migration-generation.js.map +1 -1
  79. package/dist/commands/runner/blueprint/model-generation.js +2 -2
  80. package/dist/commands/runner/blueprint/model-generation.js.map +1 -1
  81. package/dist/commands/runner/blueprint/runner.js +7 -8
  82. package/dist/commands/runner/blueprint/runner.js.map +1 -1
  83. package/dist/commands/runner/blueprint/seeder-generation.js +3 -3
  84. package/dist/commands/runner/blueprint/seeder-generation.js.map +1 -1
  85. package/dist/commands/runner/da-migration.js +1 -2
  86. package/dist/commands/runner/da-migration.js.map +1 -1
  87. package/dist/commands/runner/generate-docs.d.ts +2 -0
  88. package/dist/commands/runner/generate-docs.js +400 -0
  89. package/dist/commands/runner/generate-docs.js.map +1 -0
  90. package/dist/commands/runner/migration.js +1 -1
  91. package/dist/commands/runner/migration.js.map +1 -1
  92. package/dist/commands/runner/seeder.js +1 -1
  93. package/dist/commands/runner/seeder.js.map +1 -1
  94. package/dist/commands/stubs/index.d.ts +4 -4
  95. package/dist/commands/stubs/index.js +4 -4
  96. package/dist/commands/stubs/index.js.map +1 -1
  97. package/dist/context/context.js +6 -0
  98. package/dist/context/context.js.map +1 -1
  99. package/dist/controller/controller.d.ts +17 -30
  100. package/dist/controller/controller.js +39 -121
  101. package/dist/controller/controller.js.map +1 -1
  102. package/dist/controller/response.d.ts +6 -0
  103. package/dist/controller/response.js +63 -0
  104. package/dist/controller/response.js.map +1 -0
  105. package/dist/controller/storage.d.ts +9 -0
  106. package/dist/controller/storage.js +56 -0
  107. package/dist/controller/storage.js.map +1 -0
  108. package/dist/conversion/conversion.d.ts +3 -0
  109. package/dist/conversion/conversion.js +28 -4
  110. package/dist/conversion/conversion.js.map +1 -1
  111. package/dist/conversion/date.d.ts +1 -0
  112. package/dist/conversion/date.js +77 -0
  113. package/dist/conversion/date.js.map +1 -0
  114. package/dist/index.d.ts +2 -0
  115. package/dist/index.js +2 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/logger/logger.js +33 -0
  118. package/dist/logger/logger.js.map +1 -1
  119. package/dist/mail/mail.js +6 -6
  120. package/dist/mail/mail.js.map +1 -1
  121. package/dist/middleware/access-log.d.ts +31 -0
  122. package/dist/middleware/access-log.js +13 -0
  123. package/dist/middleware/access-log.js.map +1 -0
  124. package/dist/middleware/auth.d.ts +37 -0
  125. package/dist/middleware/auth.js +16 -0
  126. package/dist/middleware/auth.js.map +1 -0
  127. package/dist/middleware/body-parse.d.ts +35 -0
  128. package/dist/middleware/body-parse.js +87 -0
  129. package/dist/middleware/body-parse.js.map +1 -0
  130. package/dist/middleware/context.d.ts +29 -0
  131. package/dist/middleware/context.js +8 -0
  132. package/dist/middleware/context.js.map +1 -0
  133. package/dist/middleware/cors.d.ts +31 -0
  134. package/dist/middleware/cors.js +27 -0
  135. package/dist/middleware/cors.js.map +1 -0
  136. package/dist/middleware/error-handler.d.ts +33 -0
  137. package/dist/middleware/error-handler.js +17 -0
  138. package/dist/middleware/error-handler.js.map +1 -0
  139. package/dist/middleware/middleware.d.ts +31 -10
  140. package/dist/middleware/middleware.js +41 -209
  141. package/dist/middleware/middleware.js.map +1 -1
  142. package/dist/middleware/private.d.ts +29 -0
  143. package/dist/middleware/private.js +8 -0
  144. package/dist/middleware/private.js.map +1 -0
  145. package/dist/middleware/rate-limiter.d.ts +32 -0
  146. package/dist/middleware/rate-limiter.js +30 -0
  147. package/dist/middleware/rate-limiter.js.map +1 -0
  148. package/dist/notification/index.d.ts +1 -0
  149. package/dist/notification/index.js +2 -0
  150. package/dist/notification/index.js.map +1 -0
  151. package/dist/notification/notification.d.ts +16 -0
  152. package/dist/notification/notification.js +64 -0
  153. package/dist/notification/notification.js.map +1 -0
  154. package/dist/permission/permission.js +9 -0
  155. package/dist/permission/permission.js.map +1 -1
  156. package/dist/registry/registry.d.ts +0 -6
  157. package/dist/registry/registry.js +6 -6
  158. package/dist/registry/registry.js.map +1 -1
  159. package/dist/storage/storage.d.ts +3 -3
  160. package/dist/storage/storage.js.map +1 -1
  161. package/dist/validation/validation.js +43 -51
  162. package/dist/validation/validation.js.map +1 -1
  163. package/package.json +4 -4
  164. package/src/auth/auth.ts +21 -252
  165. package/src/auth/create-access-token.ts +29 -0
  166. package/src/auth/create-user-mail-token.ts +24 -0
  167. package/src/auth/helpers/generate-agent-id.ts +8 -0
  168. package/src/auth/helpers/get-request-ip.ts +3 -0
  169. package/src/auth/helpers/get-user-permissions.ts +15 -0
  170. package/src/auth/helpers/index.ts +3 -0
  171. package/src/auth/list-user-sessions.ts +11 -0
  172. package/src/auth/revalidate-user-permissions-by-role.ts +13 -0
  173. package/src/auth/revalidate-user-permissions.ts +26 -0
  174. package/src/auth/revoke-access-token.ts +5 -0
  175. package/src/auth/verify-access-token.ts +56 -0
  176. package/src/auth/verify-user-mail-token.ts +24 -0
  177. package/src/commands/cli.ts +19 -29
  178. package/src/commands/make/basic-controller.ts +4 -2
  179. package/src/commands/make/basic-migration.ts +5 -3
  180. package/src/commands/make/basic-model.ts +3 -1
  181. package/src/commands/make/basic-seeder.ts +3 -1
  182. package/src/commands/make/blueprint.ts +3 -1
  183. package/src/commands/make/da-migration.ts +6 -5
  184. package/src/commands/make/mail.ts +4 -2
  185. package/src/commands/make/notification.ts +3 -1
  186. package/src/commands/make/queue.ts +3 -1
  187. package/src/commands/make/resource.ts +21 -0
  188. package/src/commands/make/{light-controller.ts → skalfa-controller.ts} +10 -8
  189. package/src/commands/make/{light-model.ts → skalfa-model.ts} +12 -10
  190. package/src/commands/runner/barrels.ts +4 -0
  191. package/src/commands/runner/blueprint/controller-generation.ts +4 -2
  192. package/src/commands/runner/blueprint/documentation-generation.ts +2 -0
  193. package/src/commands/runner/blueprint/migration-generation.ts +5 -3
  194. package/src/commands/runner/blueprint/model-generation.ts +4 -2
  195. package/src/commands/runner/blueprint/runner.ts +15 -8
  196. package/src/commands/runner/blueprint/seeder-generation.ts +5 -3
  197. package/src/commands/runner/da-migration.ts +3 -2
  198. package/src/commands/runner/generate-docs.ts +495 -0
  199. package/src/commands/runner/migration.ts +1 -1
  200. package/src/commands/runner/seeder.ts +1 -1
  201. package/src/commands/stubs/index.ts +4 -4
  202. package/src/context/context.ts +23 -17
  203. package/src/controller/controller.ts +124 -239
  204. package/src/controller/response.ts +78 -0
  205. package/src/controller/storage.ts +78 -0
  206. package/src/conversion/conversion.ts +90 -64
  207. package/src/conversion/date.ts +74 -0
  208. package/src/index.ts +2 -0
  209. package/src/logger/logger.ts +217 -176
  210. package/src/mail/mail.ts +85 -85
  211. package/src/middleware/access-log.ts +15 -0
  212. package/src/middleware/auth.ts +19 -0
  213. package/src/middleware/body-parse.ts +83 -0
  214. package/src/middleware/context.ts +11 -0
  215. package/src/middleware/cors.ts +31 -0
  216. package/src/middleware/error-handler.ts +20 -0
  217. package/src/middleware/middleware.ts +91 -288
  218. package/src/middleware/private.ts +8 -0
  219. package/src/middleware/rate-limiter.ts +41 -0
  220. package/src/notification/index.ts +1 -0
  221. package/src/notification/notification.ts +86 -0
  222. package/src/permission/permission.ts +140 -136
  223. package/src/registry/registry.ts +17 -15
  224. package/src/route/route.ts +11 -11
  225. package/src/storage/storage.ts +104 -106
  226. package/src/validation/validation.ts +322 -346
  227. package/dist/auth.util.d.ts +0 -19
  228. package/dist/auth.util.js +0 -183
  229. package/dist/auth.util.js.map +0 -1
  230. package/dist/commands/make/light-controller.d.ts +0 -3
  231. package/dist/commands/make/light-controller.js.map +0 -1
  232. package/dist/commands/make/light-model.d.ts +0 -3
  233. package/dist/commands/make/light-model.js.map +0 -1
  234. package/dist/context.util.d.ts +0 -7
  235. package/dist/context.util.js +0 -11
  236. package/dist/context.util.js.map +0 -1
  237. package/dist/controller.util.d.ts +0 -118
  238. package/dist/controller.util.js +0 -144
  239. package/dist/controller.util.js.map +0 -1
  240. package/dist/conversion.util.d.ts +0 -8
  241. package/dist/conversion.util.js +0 -52
  242. package/dist/conversion.util.js.map +0 -1
  243. package/dist/db/db.d.ts +0 -84
  244. package/dist/db/db.js +0 -177
  245. package/dist/db/db.js.map +0 -1
  246. package/dist/db/index.d.ts +0 -1
  247. package/dist/db/index.js +0 -2
  248. package/dist/db/index.js.map +0 -1
  249. package/dist/db.util.d.ts +0 -84
  250. package/dist/db.util.js +0 -177
  251. package/dist/db.util.js.map +0 -1
  252. package/dist/logger.util.d.ts +0 -30
  253. package/dist/logger.util.js +0 -126
  254. package/dist/logger.util.js.map +0 -1
  255. package/dist/mail.util.d.ts +0 -21
  256. package/dist/mail.util.js +0 -53
  257. package/dist/mail.util.js.map +0 -1
  258. package/dist/middleware.util.d.ts +0 -263
  259. package/dist/middleware.util.js +0 -233
  260. package/dist/middleware.util.js.map +0 -1
  261. package/dist/model/index.d.ts +0 -3
  262. package/dist/model/index.js +0 -4
  263. package/dist/model/index.js.map +0 -1
  264. package/dist/model/model.d.ts +0 -204
  265. package/dist/model/model.js +0 -1495
  266. package/dist/model/model.js.map +0 -1
  267. package/dist/model.util.d.ts +0 -204
  268. package/dist/model.util.js +0 -1495
  269. package/dist/model.util.js.map +0 -1
  270. package/dist/permission.util.d.ts +0 -38
  271. package/dist/permission.util.js +0 -91
  272. package/dist/permission.util.js.map +0 -1
  273. package/dist/registry.util.d.ts +0 -28
  274. package/dist/registry.util.js +0 -19
  275. package/dist/registry.util.js.map +0 -1
  276. package/dist/route.util.d.ts +0 -1
  277. package/dist/route.util.js +0 -12
  278. package/dist/route.util.js.map +0 -1
  279. package/dist/storage.util.d.ts +0 -56
  280. package/dist/storage.util.js +0 -82
  281. package/dist/storage.util.js.map +0 -1
  282. package/dist/validation.util.d.ts +0 -7
  283. package/dist/validation.util.js +0 -237
  284. package/dist/validation.util.js.map +0 -1
@@ -1,346 +1,322 @@
1
- import validator from "validator"
2
- import { db } from "@skalfa/skalfa-orm"
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
- if (!db) {
231
- console.warn(`[Validation Warning] "unique" rule skipped on field "${field}" because database is not configured/installed.`);
232
- break;
233
- }
234
- const [table, column, exceptId] = param!.split(",")
235
- const query = db.table(table).where(column, value)
236
- if (exceptId) query.whereNot("id", exceptId)
237
- const existing = await query.first()
238
- if (existing) {
239
- addError(errors, field, `${field} sudah digunakan`)
240
- }
241
- break;
242
- }
243
-
244
- case "exists": {
245
- if (!db) {
246
- console.warn(`[Validation Warning] "exists" rule skipped on field "${field}" because database is not configured/installed.`);
247
- break;
248
- }
249
- const [table, column] = param!.split(",")
250
- const existing = await db.table(table).where(column, value).first()
251
- if (!existing) {
252
- addError(errors, field, `${field} tidak ditemukan di ${table}`)
253
- }
254
- break;
255
- }
256
- }
257
- }
258
- }
259
-
260
-
261
-
262
- async function nestedValidation({
263
- value,
264
- segments,
265
- rules,
266
- fieldPath,
267
- data,
268
- errors
269
- }: {
270
- value: any
271
- segments: string[]
272
- rules: ValidationRule[]
273
- fieldPath: string
274
- data: any
275
- errors: Record<string, string[]>
276
- }) {
277
- if (segments.length === 0) {
278
- await checkRules({
279
- field: fieldPath,
280
- value,
281
- rules,
282
- data,
283
- errors
284
- })
285
- return
286
- }
287
-
288
- const [segment, ...rest] = segments
289
-
290
- if (segment === "*") {
291
- if (!Array.isArray(value)) {
292
- addError(errors, fieldPath, `${fieldPath} harus berupa array`)
293
- return
294
- }
295
-
296
- for (let i = 0; i < value.length; i++) {
297
- await nestedValidation({
298
- value: value[i],
299
- segments: rest,
300
- rules,
301
- fieldPath: `${fieldPath}.${i}`,
302
- data,
303
- errors
304
- })
305
- }
306
- } else {
307
- await nestedValidation({
308
- value: value?.[segment],
309
- segments: rest,
310
- rules,
311
- fieldPath: fieldPath ? `${fieldPath}.${segment}` : segment,
312
- data,
313
- errors
314
- })
315
- }
316
- }
317
-
318
-
319
-
320
- // ==================================>
321
- // ## Validation helpers
322
- // ==================================>
323
- function getNestedValue(obj: any, path: string): any {
324
- if (!obj || typeof obj !== "object") return undefined
325
-
326
- const normalizedPath = path
327
- .replace(/\[(\w+)\]/g, '.$1')
328
- .replace(/\['([^']+)'\]/g, '.$1')
329
- .replace(/\["([^"]+)"\]/g, '.$1')
330
-
331
- return normalizedPath.split('.').reduce((acc, key) => {
332
- if (acc && Object.prototype.hasOwnProperty.call(acc, key)) {
333
- return acc[key]
334
- }
335
- return undefined
336
- }, obj)
337
- }
338
-
339
- function normalizeRules(rules: ValidationRule[] | string): ValidationRule[] {
340
- if (Array.isArray(rules)) return rules
341
- return rules.split("|") as ValidationRule[]
342
- }
343
-
344
- function addError(errors: Record<string, string[]>, field: string, message: string) {
345
- errors[field] = [...(errors[field] || []), message]
346
- }
1
+ import validator from "validator"
2
+ import { db } from "@skalfa/skalfa-orm";
3
+
4
+
5
+
6
+ // ==========================>
7
+ // ## Validation: Rules of validation
8
+ // ==========================>
9
+ export type ValidationRule =
10
+ | "required"
11
+ | "string"
12
+ | "numeric"
13
+ | "number"
14
+ | "boolean"
15
+ | "email"
16
+ | "url"
17
+ | "date"
18
+ | "confirmed"
19
+ | "array"
20
+ | `min:`
21
+ | `min:${number}`
22
+ | `max:`
23
+ | `max:${number}`
24
+ | `between:`
25
+ | `between:${number},${number}`
26
+ | `in:`
27
+ | `in:${string}`
28
+ | `not_in:`
29
+ | `not_in:${string}`
30
+ | `same:`
31
+ | `same:${string}`
32
+ | `different:`
33
+ | `different:${string}`
34
+ | `regex:`
35
+ | `regex:${string}`
36
+ | `unique:`
37
+ | `unique:${string},${string}`
38
+ | `exists:`
39
+ | `exists:${string},${string}`
40
+
41
+ export type ValidationRules = Record<string, ValidationRule[] | string>
42
+
43
+ export interface ValidationResult {
44
+ valid : boolean
45
+ errors : Record<string, string[]>
46
+ }
47
+
48
+
49
+
50
+ // =====================================>
51
+ // ## Validation: validate request data
52
+ // =====================================>
53
+ export async function validate(
54
+ data : Record<string, any>,
55
+ rules : ValidationRules
56
+ ): Promise<ValidationResult> {
57
+ const errors: Record<string, string[]> = {}
58
+
59
+ for (const field in rules) {
60
+ const fieldRules = normalizeRules(rules[field])
61
+
62
+ if (field.includes("*")) {
63
+ const segments = field.split(".")
64
+
65
+ await nestedValidation({ value: data, segments, rules: fieldRules, fieldPath: "", data, errors })
66
+
67
+ continue
68
+ }
69
+
70
+ const value = getNestedValue(data, field) ?? ""
71
+
72
+ await checkRules({ field, value, rules: fieldRules, data, errors })
73
+ }
74
+
75
+ return {
76
+ valid: Object.keys(errors).length === 0,
77
+ errors
78
+ }
79
+ }
80
+
81
+
82
+ async function checkRules({ field, value, rules, data, errors } : { field: string, value: any, rules: ValidationRule[], data: any, errors: Record<string, string[]> }) {
83
+ for (const rule of rules) {
84
+ const [name, param] = rule.split(":") as [string, string | undefined]
85
+
86
+ switch (name) {
87
+ // === BASIC ===
88
+ case "required":
89
+ if (validator.isEmpty(String(value).trim())) {
90
+ addError(errors, field, `${field} wajib diisi`)
91
+ }
92
+ break
93
+
94
+ case "string":
95
+ case "text":
96
+ if (!value) break
97
+ if (typeof value !== "string") {
98
+ addError(errors, field, `${field} harus berupa string`)
99
+ }
100
+ break
101
+
102
+ case "numeric":
103
+ case "number":
104
+ if (!value) break
105
+ if (!validator.isNumeric(String(value))) {
106
+ addError(errors, field, `${field} harus berupa angka`)
107
+ }
108
+ break
109
+
110
+ case "boolean":
111
+ if (!(value === true || value === false || value === "true" || value === "false" || value === 1 || value === 0)) {
112
+ addError(errors, field, `${field} harus berupa boolean`)
113
+ }
114
+ break
115
+
116
+ case "email":
117
+ if (!value) break
118
+ if (!validator.isEmail(String(value))) {
119
+ addError(errors, field, `${field} harus berupa email yang valid`)
120
+ }
121
+ break
122
+
123
+ case "url":
124
+ if (!value) break
125
+ if (!validator.isURL(String(value))) {
126
+ addError(errors, field, `${field} harus berupa URL yang valid`)
127
+ }
128
+ break
129
+
130
+ case "date":
131
+ if (!value) break
132
+ if (!validator.isDate(String(value))) {
133
+ addError(errors, field, `${field} harus berupa tanggal yang valid`)
134
+ }
135
+ break
136
+
137
+ // === LENGTH ===
138
+ case "min": {
139
+ if (!value) break
140
+ const min = parseInt(param!)
141
+ if (!validator.isLength(String(value), { min })) {
142
+ addError(errors, field, `${field} minimal ${min} karakter`)
143
+ }
144
+ break
145
+ }
146
+
147
+ case "max": {
148
+ if (!value) break
149
+ const max = parseInt(param!)
150
+ if (!validator.isLength(String(value), { max })) {
151
+ addError(errors, field, `${field} maksimal ${max} karakter`)
152
+ }
153
+ break
154
+ }
155
+
156
+ case "between": {
157
+ if (!value) break
158
+ const [minVal, maxVal] = param!.split(",").map(Number)
159
+ if (!validator.isLength(String(value), { min: minVal, max: maxVal })) {
160
+ addError(errors, field, `${field} harus antara ${minVal} - ${maxVal} karakter`)
161
+ }
162
+ break
163
+ }
164
+
165
+ // === SET MEMBERSHIP ===
166
+ case "in": {
167
+ if (!value) break
168
+ const allowed = param!.split(",")
169
+ if (!allowed.includes(String(value))) {
170
+ addError(errors, field, `${field} harus salah satu dari: ${allowed.join(", ")}`)
171
+ }
172
+ break
173
+ }
174
+
175
+ case "not_in": {
176
+ if (!value) break
177
+ const notAllowed = param!.split(",")
178
+ if (notAllowed.includes(String(value))) {
179
+ addError(errors, field, `${field} tidak boleh salah satu dari: ${notAllowed.join(", ")}`)
180
+ }
181
+ break
182
+ }
183
+
184
+ case "array": {
185
+ if (!value) break
186
+ if (!Array.isArray(value)) {
187
+ addError(errors, field, `${field} harus berupa array`)
188
+ }
189
+ break
190
+ }
191
+
192
+ // === RELATIONAL ===
193
+ case "confirmed":
194
+ if (value !== getNestedValue(data, `${field}_confirmation`)) {
195
+ addError(errors, field, `${field} tidak sama dengan konfirmasi`)
196
+ }
197
+ break
198
+
199
+ case "same":
200
+ if (value !== getNestedValue(data, param!)) {
201
+ addError(errors, field, `${field} harus sama dengan ${param}`)
202
+ }
203
+ break
204
+
205
+ case "different":
206
+ if (value === getNestedValue(data, param!)) {
207
+ addError(errors, field, `${field} harus berbeda dengan ${param}`)
208
+ }
209
+ break
210
+
211
+ // === REGEX ===
212
+ case "regex":
213
+ if (!value) break
214
+ try {
215
+ const pattern = new RegExp(param!)
216
+ if (!pattern.test(String(value))) {
217
+ addError(errors, field, `${field} tidak sesuai format`)
218
+ }
219
+ } catch {
220
+ addError(errors, field, `Regex rule untuk ${field} tidak valid`)
221
+ }
222
+ break
223
+
224
+ // === DATABASE VALIDATION ===
225
+ case "unique": {
226
+ if (!value) break
227
+ const [table, column, exceptId] = param!.split(",")
228
+ const query = db.table(table).where(column, value)
229
+ if (exceptId) query.whereNot("id", exceptId)
230
+
231
+ if (await db.schema.hasColumn(table, "deleted_at")) {
232
+ query.whereNull("deleted_at")
233
+ }
234
+
235
+ const existing = await query.first()
236
+ if (existing) {
237
+ addError(errors, field, `${field} sudah digunakan`)
238
+ }
239
+ break
240
+ }
241
+
242
+ case "exists": {
243
+ if (!value) break
244
+ const [table, column] = param!.split(",")
245
+ const query = db.table(table).where(column, value)
246
+
247
+ if (await db.schema.hasColumn(table, "deleted_at")) {
248
+ query.whereNull("deleted_at")
249
+ }
250
+
251
+ const existing = await query.first()
252
+ if (!existing) {
253
+ addError(errors, field, `${field} tidak ditemukan di ${table}`)
254
+ }
255
+ break
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+
262
+
263
+ async function nestedValidation({ value, segments, rules, fieldPath, data, errors }: {
264
+ value : any
265
+ segments : string[]
266
+ rules : ValidationRule[]
267
+ fieldPath : string
268
+ data : any
269
+ errors : Record<string, string[]>
270
+ }) {
271
+ if (segments.length === 0) {
272
+ await checkRules({ field: fieldPath, value, rules, data, errors })
273
+
274
+ return
275
+ }
276
+
277
+ const [segment, ...rest] = segments
278
+
279
+ if (segment === "*") {
280
+ if (!Array.isArray(value)) {
281
+ addError(errors, fieldPath, `${fieldPath} harus berupa array`)
282
+
283
+ return
284
+ }
285
+
286
+ for (let i = 0; i < value.length; i++) {
287
+ await nestedValidation({ value: value[i], segments: rest, rules, fieldPath: `${fieldPath}.${i}`, data, errors })
288
+ }
289
+ } else {
290
+ await nestedValidation({ value: value?.[segment], segments: rest, rules, fieldPath: fieldPath ? `${fieldPath}.${segment}` : segment, data, errors})
291
+ }
292
+ }
293
+
294
+
295
+
296
+ // ==================================>
297
+ // ## Validation helpers
298
+ // ==================================>
299
+ function getNestedValue(obj: any, path: string): any {
300
+ if (!obj || typeof obj !== "object") return undefined
301
+
302
+ const normalizedPath = path
303
+ .replace(/\[(\w+)\]/g, '.$1')
304
+ .replace(/\['([^']+)'\]/g, '.$1')
305
+ .replace(/\["([^"]+)"\]/g, '.$1')
306
+
307
+ return normalizedPath.split('.').reduce((acc, key) => {
308
+ if (acc && Object.prototype.hasOwnProperty.call(acc, key)) return acc[key]
309
+
310
+ return undefined
311
+ }, obj)
312
+ }
313
+
314
+ function normalizeRules(rules: ValidationRule[] | string): ValidationRule[] {
315
+ if (Array.isArray(rules)) return rules
316
+
317
+ return rules.split("|") as ValidationRule[]
318
+ }
319
+
320
+ function addError(errors: Record<string, string[]>, field: string, message: string) {
321
+ errors[field] = [...(errors[field] || []), message]
322
+ }