feat(validation): add arktype form schemas and i18n keys
Introduces field-specific validation messages so toasts name the exact field and rule that failed instead of a catch-all "tous les champs sont requis".
This commit is contained in:
@@ -47,6 +47,20 @@ export default {
|
|||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
max_char:
|
max_char:
|
||||||
'no character available | 1 character max | {max} character maximum'
|
'no character available | 1 character max | {max} character maximum',
|
||||||
|
transaction_name_required: 'Transaction name is required.',
|
||||||
|
amount_required: 'Amount is required.',
|
||||||
|
amount_must_be_positive: 'Amount must be greater than 0.',
|
||||||
|
amount_too_large: 'Amount must be less than {max}.',
|
||||||
|
currency_required: 'Currency is required.',
|
||||||
|
date_required: 'Date is required.',
|
||||||
|
pay_by_required: 'Payer is required.',
|
||||||
|
pay_for_required: 'At least one share must be assigned.',
|
||||||
|
user_id_required: 'A username is required.',
|
||||||
|
passwords_mismatch: 'Passwords must match.',
|
||||||
|
account_name_required: 'Account name is required.',
|
||||||
|
account_needs_one_user: 'At least one person must be added to the account.',
|
||||||
|
aliases_must_be_unique: 'Aliases must be unique.',
|
||||||
|
user_ids_must_be_unique: 'Usernames must be unique.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,20 @@ export default {
|
|||||||
pseudo: 'identifiant'
|
pseudo: 'identifiant'
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
max_char: 'limite atteinte | 1 caractère maximum | {max} caractères maximum'
|
max_char: 'limite atteinte | 1 caractère maximum | {max} caractères maximum',
|
||||||
|
transaction_name_required: 'Le nom de la dépense est requis.',
|
||||||
|
amount_required: 'Le montant est requis.',
|
||||||
|
amount_must_be_positive: 'Le montant doit être supérieur à 0.',
|
||||||
|
amount_too_large: 'Le montant doit être inférieur à {max}.',
|
||||||
|
currency_required: 'La devise est requise.',
|
||||||
|
date_required: 'La date est requise.',
|
||||||
|
pay_by_required: 'Le payeur est requis.',
|
||||||
|
pay_for_required: 'Au moins une part doit être attribuée.',
|
||||||
|
user_id_required: 'Un pseudo est obligatoire.',
|
||||||
|
passwords_mismatch: 'Les mots de passe doivent être identiques.',
|
||||||
|
account_name_required: 'Le nom du compte est requis.',
|
||||||
|
account_needs_one_user: 'Au moins une personne doit être ajoutée au compte.',
|
||||||
|
aliases_must_be_unique: 'Les alias doivent être uniques.',
|
||||||
|
user_ids_must_be_unique: 'Les identifiants doivent être uniques.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/schemas/account.ts
Normal file
20
src/schemas/account.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { type } from 'arktype'
|
||||||
|
|
||||||
|
export const accountFormSchema = type({
|
||||||
|
name: 'unknown',
|
||||||
|
aliases: 'unknown',
|
||||||
|
userIds: 'unknown'
|
||||||
|
}).narrow((data, ctx) => {
|
||||||
|
if (typeof data.name !== 'string' || data.name.trim().length === 0) {
|
||||||
|
ctx.reject({ message: 'validation.account_name_required', path: ['name'] })
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data.aliases) || data.aliases.length === 0) {
|
||||||
|
ctx.reject({ message: 'validation.account_needs_one_user', path: ['aliases'] })
|
||||||
|
} else if (new Set(data.aliases).size !== data.aliases.length) {
|
||||||
|
ctx.reject({ message: 'validation.aliases_must_be_unique', path: ['aliases'] })
|
||||||
|
}
|
||||||
|
if (Array.isArray(data.userIds) && new Set(data.userIds).size !== data.userIds.length) {
|
||||||
|
ctx.reject({ message: 'validation.user_ids_must_be_unique', path: ['userIds'] })
|
||||||
|
}
|
||||||
|
return !ctx.hasError()
|
||||||
|
})
|
||||||
24
src/schemas/transaction.ts
Normal file
24
src/schemas/transaction.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { type } from 'arktype'
|
||||||
|
|
||||||
|
export const MAX_TRANSACTION_AMOUNT = 1_000_000
|
||||||
|
|
||||||
|
export const transactionFormSchema = type({
|
||||||
|
name: 'unknown',
|
||||||
|
amount: 'unknown',
|
||||||
|
payBy: 'unknown'
|
||||||
|
}).narrow((data, ctx) => {
|
||||||
|
if (typeof data.name !== 'string' || data.name.trim().length === 0) {
|
||||||
|
ctx.reject({ message: 'validation.transaction_name_required', path: ['name'] })
|
||||||
|
}
|
||||||
|
if (typeof data.amount !== 'number' || Number.isNaN(data.amount)) {
|
||||||
|
ctx.reject({ message: 'validation.amount_required', path: ['amount'] })
|
||||||
|
} else if (data.amount <= 0) {
|
||||||
|
ctx.reject({ message: 'validation.amount_must_be_positive', path: ['amount'] })
|
||||||
|
} else if (data.amount > MAX_TRANSACTION_AMOUNT) {
|
||||||
|
ctx.reject({ message: 'validation.amount_too_large', path: ['amount'] })
|
||||||
|
}
|
||||||
|
if (typeof data.payBy !== 'string' || data.payBy.length === 0) {
|
||||||
|
ctx.reject({ message: 'validation.pay_by_required', path: ['payBy'] })
|
||||||
|
}
|
||||||
|
return !ctx.hasError()
|
||||||
|
})
|
||||||
15
src/schemas/user.ts
Normal file
15
src/schemas/user.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { type } from 'arktype'
|
||||||
|
|
||||||
|
export const signupFormSchema = type({
|
||||||
|
userId: 'unknown',
|
||||||
|
password: 'unknown',
|
||||||
|
confirmPassword: 'unknown'
|
||||||
|
}).narrow((data, ctx) => {
|
||||||
|
if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
|
||||||
|
ctx.reject({ message: 'validation.user_id_required', path: ['userId'] })
|
||||||
|
}
|
||||||
|
if (data.password !== data.confirmPassword) {
|
||||||
|
ctx.reject({ message: 'validation.passwords_mismatch', path: ['confirmPassword'] })
|
||||||
|
}
|
||||||
|
return !ctx.hasError()
|
||||||
|
})
|
||||||
29
src/utils/validate.ts
Normal file
29
src/utils/validate.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ArkErrors } from 'arktype'
|
||||||
|
import type { Type } from 'arktype'
|
||||||
|
import type { Composer } from 'vue-i18n'
|
||||||
|
import queueNotifService from '@/services/QueueNotifService'
|
||||||
|
|
||||||
|
type TranslateFn = Composer['t']
|
||||||
|
type MessageParams = Record<string, unknown>
|
||||||
|
export type ParamsByPath = Record<string, MessageParams>
|
||||||
|
|
||||||
|
export function validateAndNotify(
|
||||||
|
schema: Type,
|
||||||
|
data: unknown,
|
||||||
|
t: TranslateFn,
|
||||||
|
paramsByPath: ParamsByPath = {}
|
||||||
|
): boolean {
|
||||||
|
const result = schema(data)
|
||||||
|
if (!(result instanceof ArkErrors)) return true
|
||||||
|
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const error of result) {
|
||||||
|
const pathKey = error.path.join('.')
|
||||||
|
// Skip duplicate field-level errors at the same path so the user gets one toast per field.
|
||||||
|
if (seen.has(pathKey)) continue
|
||||||
|
seen.add(pathKey)
|
||||||
|
const params = paramsByPath[pathKey] ?? {}
|
||||||
|
queueNotifService.error(t(error.message, params))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user