@kava/kava-api-core 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +40 -0
- package/dist/{auth.util.d.ts → auth/auth.d.ts} +1 -5
- package/dist/{auth.util.js → auth/auth.js} +38 -30
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/{context.util.js → context/context.js} +1 -1
- package/dist/context/context.js.map +1 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{controller.util.js → controller/controller.js} +1 -1
- package/dist/controller/controller.js.map +1 -0
- package/dist/controller/index.d.ts +1 -0
- package/dist/controller/index.js +2 -0
- package/dist/controller/index.js.map +1 -0
- package/dist/{conversion.util.js → conversion/conversion.js} +1 -1
- package/dist/conversion/conversion.js.map +1 -0
- package/dist/conversion/index.d.ts +1 -0
- package/dist/conversion/index.js +2 -0
- package/dist/conversion/index.js.map +1 -0
- package/dist/{db.util.d.ts → db/db.d.ts} +9 -5
- package/dist/{db.util.js → db/db.js} +40 -29
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +14 -13
- package/dist/index.js.map +1 -1
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.js +2 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/{logger.util.js → logger/logger.js} +16 -7
- package/dist/logger/logger.js.map +1 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/{mail.util.js → mail/mail.js} +1 -1
- package/dist/mail/mail.js.map +1 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/{middleware.util.js → middleware/middleware.js} +1 -1
- package/dist/middleware/middleware.js.map +1 -0
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +2 -0
- package/dist/model/index.js.map +1 -0
- package/dist/{model.util.js → model/model.js} +1 -1
- package/dist/model/model.js.map +1 -0
- package/dist/permission/index.d.ts +1 -0
- package/dist/permission/index.js +2 -0
- package/dist/permission/index.js.map +1 -0
- package/dist/{permission.util.js → permission/permission.js} +1 -1
- package/dist/permission/permission.js.map +1 -0
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/registry.d.ts +28 -0
- package/dist/registry/registry.js +19 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/route/index.d.ts +1 -0
- package/dist/route/index.js +2 -0
- package/dist/route/index.js.map +1 -0
- package/dist/{route.util.js → route/route.js} +1 -1
- package/dist/route/route.js.map +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/{storage.util.js → storage/storage.js} +2 -2
- package/dist/storage/storage.js.map +1 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/{validation.util.js → validation/validation.js} +1 -1
- package/dist/validation/validation.js.map +1 -0
- package/package.json +2 -2
- package/src/{auth.util.ts → auth/auth.ts} +255 -241
- package/src/auth/index.ts +1 -0
- package/src/{context.util.ts → context/context.ts} +17 -17
- package/src/context/index.ts +1 -0
- package/src/{controller.util.ts → controller/controller.ts} +236 -236
- package/src/controller/index.ts +1 -0
- package/src/{conversion.util.ts → conversion/conversion.ts} +64 -64
- package/src/conversion/index.ts +1 -0
- package/src/{db.util.ts → db/db.ts} +420 -405
- package/src/db/index.ts +1 -0
- package/src/index.ts +14 -13
- package/src/logger/index.ts +1 -0
- package/src/{logger.util.ts → logger/logger.ts} +176 -169
- package/src/mail/index.ts +1 -0
- package/src/{mail.util.ts → mail/mail.ts} +85 -85
- package/src/middleware/index.ts +1 -0
- package/src/{middleware.util.ts → middleware/middleware.ts} +288 -288
- package/src/model/index.ts +1 -0
- package/src/{model.util.ts → model/model.ts} +2210 -2210
- package/src/permission/index.ts +1 -0
- package/src/{permission.util.ts → permission/permission.ts} +136 -136
- package/src/registry/index.ts +1 -0
- package/src/registry/registry.ts +37 -0
- package/src/route/index.ts +1 -0
- package/src/{route.util.ts → route/route.ts} +11 -11
- package/src/storage/index.ts +1 -0
- package/src/{storage.util.ts → storage/storage.ts} +101 -101
- package/src/validation/index.ts +1 -0
- package/src/{validation.util.ts → validation/validation.ts} +338 -338
- package/tsconfig.json +1 -1
- package/bun.lock +0 -160
- package/dist/auth.util.js.map +0 -1
- package/dist/context.util.js.map +0 -1
- package/dist/controller.util.js.map +0 -1
- package/dist/conversion.util.js.map +0 -1
- package/dist/db.util.js.map +0 -1
- package/dist/logger.util.js.map +0 -1
- package/dist/mail.util.js.map +0 -1
- package/dist/middleware.util.js.map +0 -1
- package/dist/model.util.js.map +0 -1
- package/dist/permission.util.js.map +0 -1
- package/dist/route.util.js.map +0 -1
- package/dist/storage.util.js.map +0 -1
- package/dist/validation.util.js.map +0 -1
- /package/dist/{context.util.d.ts → context/context.d.ts} +0 -0
- /package/dist/{controller.util.d.ts → controller/controller.d.ts} +0 -0
- /package/dist/{conversion.util.d.ts → conversion/conversion.d.ts} +0 -0
- /package/dist/{logger.util.d.ts → logger/logger.d.ts} +0 -0
- /package/dist/{mail.util.d.ts → mail/mail.d.ts} +0 -0
- /package/dist/{middleware.util.d.ts → middleware/middleware.d.ts} +0 -0
- /package/dist/{model.util.d.ts → model/model.d.ts} +0 -0
- /package/dist/{permission.util.d.ts → permission/permission.d.ts} +0 -0
- /package/dist/{route.util.d.ts → route/route.d.ts} +0 -0
- /package/dist/{storage.util.d.ts → storage/storage.d.ts} +0 -0
- /package/dist/{validation.util.d.ts → validation/validation.d.ts} +0 -0
|
@@ -1,338 +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
|
-
}
|
|
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
|
+
}
|