@skalfa/skalfa-api-core 1.0.2 → 1.0.7

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 (272) 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 +5 -3
  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/{light-controller.js → skalfa-controller.js} +3 -3
  68. package/dist/commands/make/skalfa-controller.js.map +1 -0
  69. package/dist/commands/make/{light-model.js → skalfa-model.js} +4 -4
  70. package/dist/commands/make/skalfa-model.js.map +1 -0
  71. package/dist/commands/runner/barrels.js.map +1 -1
  72. package/dist/commands/runner/blueprint/controller-generation.js.map +1 -1
  73. package/dist/commands/runner/blueprint/documentation-generation.js.map +1 -1
  74. package/dist/commands/runner/blueprint/migration-generation.js +1 -1
  75. package/dist/commands/runner/blueprint/migration-generation.js.map +1 -1
  76. package/dist/commands/runner/blueprint/model-generation.js.map +1 -1
  77. package/dist/commands/runner/blueprint/runner.js +2 -2
  78. package/dist/commands/runner/blueprint/runner.js.map +1 -1
  79. package/dist/commands/runner/blueprint/seeder-generation.js +1 -1
  80. package/dist/commands/runner/blueprint/seeder-generation.js.map +1 -1
  81. package/dist/commands/runner/da-migration.js +1 -2
  82. package/dist/commands/runner/da-migration.js.map +1 -1
  83. package/dist/commands/runner/migration.js +1 -1
  84. package/dist/commands/runner/migration.js.map +1 -1
  85. package/dist/commands/runner/seeder.js +1 -1
  86. package/dist/commands/runner/seeder.js.map +1 -1
  87. package/dist/context/context.js +6 -0
  88. package/dist/context/context.js.map +1 -1
  89. package/dist/controller/controller.d.ts +17 -30
  90. package/dist/controller/controller.js +39 -121
  91. package/dist/controller/controller.js.map +1 -1
  92. package/dist/controller/response.d.ts +6 -0
  93. package/dist/controller/response.js +63 -0
  94. package/dist/controller/response.js.map +1 -0
  95. package/dist/controller/storage.d.ts +9 -0
  96. package/dist/controller/storage.js +56 -0
  97. package/dist/controller/storage.js.map +1 -0
  98. package/dist/conversion/conversion.d.ts +3 -0
  99. package/dist/conversion/conversion.js +28 -4
  100. package/dist/conversion/conversion.js.map +1 -1
  101. package/dist/conversion/date.d.ts +1 -0
  102. package/dist/conversion/date.js +77 -0
  103. package/dist/conversion/date.js.map +1 -0
  104. package/dist/index.d.ts +2 -0
  105. package/dist/index.js +2 -0
  106. package/dist/index.js.map +1 -1
  107. package/dist/logger/logger.js +33 -0
  108. package/dist/logger/logger.js.map +1 -1
  109. package/dist/mail/mail.js +6 -6
  110. package/dist/mail/mail.js.map +1 -1
  111. package/dist/middleware/access-log.d.ts +31 -0
  112. package/dist/middleware/access-log.js +13 -0
  113. package/dist/middleware/access-log.js.map +1 -0
  114. package/dist/middleware/auth.d.ts +37 -0
  115. package/dist/middleware/auth.js +16 -0
  116. package/dist/middleware/auth.js.map +1 -0
  117. package/dist/middleware/body-parse.d.ts +35 -0
  118. package/dist/middleware/body-parse.js +87 -0
  119. package/dist/middleware/body-parse.js.map +1 -0
  120. package/dist/middleware/context.d.ts +29 -0
  121. package/dist/middleware/context.js +8 -0
  122. package/dist/middleware/context.js.map +1 -0
  123. package/dist/middleware/cors.d.ts +31 -0
  124. package/dist/middleware/cors.js +27 -0
  125. package/dist/middleware/cors.js.map +1 -0
  126. package/dist/middleware/error-handler.d.ts +33 -0
  127. package/dist/middleware/error-handler.js +17 -0
  128. package/dist/middleware/error-handler.js.map +1 -0
  129. package/dist/middleware/middleware.d.ts +31 -10
  130. package/dist/middleware/middleware.js +41 -209
  131. package/dist/middleware/middleware.js.map +1 -1
  132. package/dist/middleware/private.d.ts +29 -0
  133. package/dist/middleware/private.js +8 -0
  134. package/dist/middleware/private.js.map +1 -0
  135. package/dist/middleware/rate-limiter.d.ts +32 -0
  136. package/dist/middleware/rate-limiter.js +30 -0
  137. package/dist/middleware/rate-limiter.js.map +1 -0
  138. package/dist/notification/index.d.ts +1 -0
  139. package/dist/notification/index.js +2 -0
  140. package/dist/notification/index.js.map +1 -0
  141. package/dist/notification/notification.d.ts +16 -0
  142. package/dist/notification/notification.js +64 -0
  143. package/dist/notification/notification.js.map +1 -0
  144. package/dist/permission/permission.js +9 -0
  145. package/dist/permission/permission.js.map +1 -1
  146. package/dist/registry/registry.d.ts +0 -6
  147. package/dist/registry/registry.js +6 -6
  148. package/dist/registry/registry.js.map +1 -1
  149. package/dist/storage/storage.d.ts +3 -3
  150. package/dist/storage/storage.js.map +1 -1
  151. package/dist/validation/validation.js +43 -51
  152. package/dist/validation/validation.js.map +1 -1
  153. package/package.json +4 -4
  154. package/src/auth/auth.ts +21 -252
  155. package/src/auth/create-access-token.ts +29 -0
  156. package/src/auth/create-user-mail-token.ts +24 -0
  157. package/src/auth/helpers/generate-agent-id.ts +8 -0
  158. package/src/auth/helpers/get-request-ip.ts +3 -0
  159. package/src/auth/helpers/get-user-permissions.ts +15 -0
  160. package/src/auth/helpers/index.ts +3 -0
  161. package/src/auth/list-user-sessions.ts +11 -0
  162. package/src/auth/revalidate-user-permissions-by-role.ts +13 -0
  163. package/src/auth/revalidate-user-permissions.ts +26 -0
  164. package/src/auth/revoke-access-token.ts +5 -0
  165. package/src/auth/verify-access-token.ts +56 -0
  166. package/src/auth/verify-user-mail-token.ts +24 -0
  167. package/src/commands/cli.ts +5 -3
  168. package/src/commands/make/basic-controller.ts +3 -1
  169. package/src/commands/make/basic-migration.ts +5 -3
  170. package/src/commands/make/basic-model.ts +3 -1
  171. package/src/commands/make/basic-seeder.ts +3 -1
  172. package/src/commands/make/blueprint.ts +3 -1
  173. package/src/commands/make/da-migration.ts +6 -5
  174. package/src/commands/make/mail.ts +4 -2
  175. package/src/commands/make/notification.ts +3 -1
  176. package/src/commands/make/queue.ts +3 -1
  177. package/src/commands/make/resource.ts +21 -0
  178. package/src/commands/make/{light-controller.ts → skalfa-controller.ts} +4 -2
  179. package/src/commands/make/{light-model.ts → skalfa-model.ts} +5 -3
  180. package/src/commands/runner/barrels.ts +4 -0
  181. package/src/commands/runner/blueprint/controller-generation.ts +2 -0
  182. package/src/commands/runner/blueprint/documentation-generation.ts +2 -0
  183. package/src/commands/runner/blueprint/migration-generation.ts +3 -1
  184. package/src/commands/runner/blueprint/model-generation.ts +2 -0
  185. package/src/commands/runner/blueprint/runner.ts +10 -2
  186. package/src/commands/runner/blueprint/seeder-generation.ts +3 -1
  187. package/src/commands/runner/da-migration.ts +3 -2
  188. package/src/commands/runner/migration.ts +1 -1
  189. package/src/commands/runner/seeder.ts +1 -1
  190. package/src/context/context.ts +23 -17
  191. package/src/controller/controller.ts +124 -239
  192. package/src/controller/response.ts +78 -0
  193. package/src/controller/storage.ts +78 -0
  194. package/src/conversion/conversion.ts +90 -64
  195. package/src/conversion/date.ts +74 -0
  196. package/src/index.ts +2 -0
  197. package/src/logger/logger.ts +217 -176
  198. package/src/mail/mail.ts +85 -85
  199. package/src/middleware/access-log.ts +15 -0
  200. package/src/middleware/auth.ts +19 -0
  201. package/src/middleware/body-parse.ts +83 -0
  202. package/src/middleware/context.ts +11 -0
  203. package/src/middleware/cors.ts +31 -0
  204. package/src/middleware/error-handler.ts +20 -0
  205. package/src/middleware/middleware.ts +91 -288
  206. package/src/middleware/private.ts +8 -0
  207. package/src/middleware/rate-limiter.ts +41 -0
  208. package/src/notification/index.ts +1 -0
  209. package/src/notification/notification.ts +86 -0
  210. package/src/permission/permission.ts +140 -136
  211. package/src/registry/registry.ts +17 -15
  212. package/src/route/route.ts +11 -11
  213. package/src/storage/storage.ts +104 -106
  214. package/src/validation/validation.ts +322 -346
  215. package/dist/auth.util.d.ts +0 -19
  216. package/dist/auth.util.js +0 -183
  217. package/dist/auth.util.js.map +0 -1
  218. package/dist/commands/make/light-controller.js.map +0 -1
  219. package/dist/commands/make/light-model.js.map +0 -1
  220. package/dist/context.util.d.ts +0 -7
  221. package/dist/context.util.js +0 -11
  222. package/dist/context.util.js.map +0 -1
  223. package/dist/controller.util.d.ts +0 -118
  224. package/dist/controller.util.js +0 -144
  225. package/dist/controller.util.js.map +0 -1
  226. package/dist/conversion.util.d.ts +0 -8
  227. package/dist/conversion.util.js +0 -52
  228. package/dist/conversion.util.js.map +0 -1
  229. package/dist/db/db.d.ts +0 -84
  230. package/dist/db/db.js +0 -177
  231. package/dist/db/db.js.map +0 -1
  232. package/dist/db/index.d.ts +0 -1
  233. package/dist/db/index.js +0 -2
  234. package/dist/db/index.js.map +0 -1
  235. package/dist/db.util.d.ts +0 -84
  236. package/dist/db.util.js +0 -177
  237. package/dist/db.util.js.map +0 -1
  238. package/dist/logger.util.d.ts +0 -30
  239. package/dist/logger.util.js +0 -126
  240. package/dist/logger.util.js.map +0 -1
  241. package/dist/mail.util.d.ts +0 -21
  242. package/dist/mail.util.js +0 -53
  243. package/dist/mail.util.js.map +0 -1
  244. package/dist/middleware.util.d.ts +0 -263
  245. package/dist/middleware.util.js +0 -233
  246. package/dist/middleware.util.js.map +0 -1
  247. package/dist/model/index.d.ts +0 -3
  248. package/dist/model/index.js +0 -4
  249. package/dist/model/index.js.map +0 -1
  250. package/dist/model/model.d.ts +0 -204
  251. package/dist/model/model.js +0 -1495
  252. package/dist/model/model.js.map +0 -1
  253. package/dist/model.util.d.ts +0 -204
  254. package/dist/model.util.js +0 -1495
  255. package/dist/model.util.js.map +0 -1
  256. package/dist/permission.util.d.ts +0 -38
  257. package/dist/permission.util.js +0 -91
  258. package/dist/permission.util.js.map +0 -1
  259. package/dist/registry.util.d.ts +0 -28
  260. package/dist/registry.util.js +0 -19
  261. package/dist/registry.util.js.map +0 -1
  262. package/dist/route.util.d.ts +0 -1
  263. package/dist/route.util.js +0 -12
  264. package/dist/route.util.js.map +0 -1
  265. package/dist/storage.util.d.ts +0 -56
  266. package/dist/storage.util.js +0 -82
  267. package/dist/storage.util.js.map +0 -1
  268. package/dist/validation.util.d.ts +0 -7
  269. package/dist/validation.util.js +0 -237
  270. package/dist/validation.util.js.map +0 -1
  271. /package/dist/commands/make/{light-controller.d.ts → skalfa-controller.d.ts} +0 -0
  272. /package/dist/commands/make/{light-model.d.ts → skalfa-model.d.ts} +0 -0
@@ -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
+ }