Композиция в Joi — это построение сложных схем валидации из более простых и переиспользуемых частей. Вместо дублирования правил создаются базовые схемы, которые затем расширяются, комбинируются и наследуются.
Библиотека предоставляет несколько ключевых механизмов:
concat() — объединение схемappend() — расширение объекта новыми полямиkeys() — переопределение структуры объектаfork() — массовое изменение правилextract() — извлечение вложенных схемlink() — ссылки между схемамиshared() — повторное использование именованных
схемalter() и tailor() — адаптация схем под
разные сценарииwhen() — условная композицияМетод concat() объединяет две совместимые схемы в
одну.
const Joi = require('joi');
const baseSchema = Joi.string().min(3);
const extendedSchema = baseSchema.concat(
Joi.string().max(10)
);
console.log(extendedSchema.validate('hello'));
Результирующая схема:
Joi.string().min(3).max(10)
При несовместимых типах Joi выбрасывает ошибку.
const schema1 = Joi.string();
const schema2 = Joi.number();
const schema = schema1.concat(schema2);
Ошибка:
Cannot merge type string with another type: number
const credentialsSchema = Joi.object({
login: Joi.string().required()
});
const profileSchema = Joi.object({
age: Joi.number().min(18)
});
const userSchema = credentialsSchema.concat(profileSchema);
Результат:
{
login: Joi.string().required(),
age: Joi.number().min(18)
}
Метод append() добавляет новые ключи в объектную
схему.
const baseSchema = Joi.object({
name: Joi.string()
});
const extendedSchema = baseSchema.append({
email: Joi.string().email()
});
Результирующая схема:
{
name: Joi.string(),
email: Joi.string().email()
}
const entitySchema = Joi.object({
id: Joi.number().required(),
createdAt: Joi.date()
});
const articleSchema = entitySchema.append({
title: Joi.string().required(),
content: Joi.string().required()
});
const commentSchema = entitySchema.append({
message: Joi.string().required()
});
Такой подход позволяет централизованно хранить общие правила.
Метод keys() переопределяет или дополняет объектную
схему.
const schema = Joi.object({
name: Joi.string()
});
const extended = schema.keys({
age: Joi.number()
});
const schema = Joi.object({
role: Joi.string()
});
const updated = schema.keys({
role: Joi.string().valid('admin', 'user')
});
Теперь поле role ограничено списком допустимых
значений.
Метод fork() позволяет массово изменять правила
существующих полей.
Особенно полезен при разделении схем:
const userSchema = Joi.object({
name: Joi.string(),
email: Joi.string().email(),
password: Joi.string()
});
const createSchema = userSchema.fork(
['name', 'email', 'password'],
field => field.required()
);
const updateSchema = userSchema.fork(
['password'],
field => field.optional()
);
const schema = Joi.object({
profile: Joi.object({
contacts: Joi.object({
phone: Joi.string(),
email: Joi.string()
})
})
});
const modified = schema.fork(
['profile.contacts.phone'],
field => field.required()
);
Метод extract() позволяет получить вложенную схему по
пути.
const userSchema = Joi.object({
profile: Joi.object({
email: Joi.string().email()
})
});
const emailSchema = userSchema.extract('profile.email');
console.log(
emailSchema.validate('admin@example.com')
);
link() используется для построения взаимосвязанных схем
без дублирования.
const personSchema = Joi.object({
firstName: Joi.string(),
lastName: Joi.string()
}).id('person');
const schema = Joi.object({
author: Joi.link('#person'),
editor: Joi.link('#person')
});
const addressSchema = Joi.object({
city: Joi.string(),
street: Joi.string()
}).id('address');
const companySchema = Joi.object({
legalAddress: Joi.link('#address'),
postalAddress: Joi.link('#address')
});
Метод shared() регистрирует схему для повторного
использования.
const emailSchema = Joi.string()
.email()
.id('email');
const schema = Joi.object({
primary: Joi.link('#email'),
secondary: Joi.link('#email')
}).shared(emailSchema);
Метод when() позволяет динамически изменять структуру
схемы.
const schema = Joi.object({
role: Joi.string().valid('user', 'admin'),
permissions: Joi.when('role', {
is: 'admin',
then: Joi.array().items(Joi.string()).required(),
otherwise: Joi.forbidden()
})
});
const schema = Joi.object({
type: Joi.string(),
value: Joi.when('type', [
{
is: 'string',
then: Joi.string()
},
{
is: 'number',
then: Joi.number()
}
])
});
Механизм alter() позволяет заранее определить варианты
изменения схемы.
tailor() применяет нужную модификацию.
const schema = Joi.object({
name: Joi.string().alter({
create: field => field.required(),
update: field => field.optional()
}),
email: Joi.string().email().alter({
create: field => field.required(),
update: field => field.optional()
})
});
const createSchema = schema.tailor('create');
const updateSchema = schema.tailor('update');
createSchema.validate({
name: 'Alex'
});
Ошибка:
"email" is required
updateSchema.validate({
name: 'Alex'
});
Ошибки не будет.
Сложные схемы обычно состоят из множества переиспользуемых компонентов.
const addressSchema = Joi.object({
city: Joi.string().required(),
zip: Joi.string().required()
});
const contactSchema = Joi.object({
phone: Joi.string(),
email: Joi.string().email()
});
const userSchema = Joi.object({
name: Joi.string().required(),
address: addressSchema,
contacts: contactSchema
});
const itemSchema = Joi.object({
title: Joi.string(),
price: Joi.number()
});
const cartSchema = Joi.object({
items: Joi.array().items(itemSchema)
});
Композиция особенно важна при моделировании доменной структуры приложения.
const entitySchema = Joi.object({
id: Joi.number().required(),
createdAt: Joi.date().required(),
updatedAt: Joi.date()
});
const userSchema = entitySchema.append({
name: Joi.string().required(),
email: Joi.string().email()
});
const articleSchema = entitySchema.append({
title: Joi.string(),
body: Joi.string()
});
const baseUserSchema = Joi.object({
role: Joi.string()
});
const adminSchema = baseUserSchema.append({
permissions: Joi.array()
});
const finalSchema = adminSchema.when(
Joi.object({ role: Joi.valid('admin') }).unknown(),
{
then: Joi.object({
permissions: Joi.required()
})
}
);
function createPaginationSchema(entitySchema) {
return Joi.object({
items: Joi.array().items(entitySchema),
total: Joi.number(),
page: Joi.number(),
limit: Joi.number()
});
}
const paginatedUsers =
createPaginationSchema(userSchema);
const paginatedArticles =
createPaginationSchema(articleSchema);
Joi позволяет строить многоуровневые схемы практически любой сложности.
const geoSchema = Joi.object({
lat: Joi.number(),
lng: Joi.number()
});
const addressSchema = Joi.object({
city: Joi.string(),
street: Joi.string(),
geo: geoSchema
});
const companySchema = Joi.object({
name: Joi.string(),
address: addressSchema
});
const userSchema = Joi.object({
profile: Joi.object({
firstName: Joi.string(),
lastName: Joi.string()
}),
company: companySchema
});
const requiredString = Joi.string().required();
const schema = Joi.object({
firstName: requiredString,
lastName: requiredString,
city: requiredString
});
alternatives() используется для объединения нескольких
вариантов схем.
const schema = Joi.alternatives().try(
Joi.string(),
Joi.number(),
Joi.boolean()
);
const adminSchema = Joi.object({
role: Joi.valid('admin'),
permissions: Joi.array().required()
});
const userSchema = Joi.object({
role: Joi.valid('user')
});
const schema = Joi.alternatives().try(
adminSchema,
userSchema
);
schemas/
├── common/
├── user/
├── article/
├── comment/
└── shared/
base.schema.js
user.request.schema.js
user.model.schema.js
const idSchema = Joi.number().integer().positive();
const timestampSchema = Joi.object({
createdAt: Joi.date(),
updatedAt: Joi.date()
});
const userSchema = Joi.object({
id: idSchema,
email: Joi.string().email(),
name: Joi.string()
});
const commentSchema = timestampSchema.append({
id: idSchema,
message: Joi.string(),
author: userSchema
});
const articleSchema = timestampSchema.append({
id: idSchema,
title: Joi.string(),
body: Joi.string(),
author: userSchema,
comments: Joi.array().items(commentSchema)
});
Joi.string().concat(Joi.number());
const schema = Joi.object({
name: Joi.string().required()
});
const modified = schema.keys({
name: Joi.string()
});
Флаг required() будет потерян.
Слишком глубокая композиция усложняет:
const emailSchema = Joi.string().email();
const passwordSchema = Joi.string().min(8);
function createEntitySchema(fields) {
return Joi.object({
id: Joi.number(),
...fields
});
}
createSchema
updateSchema
patchSchema
adminSchema
publicSchema
Повторяющиеся правила должны храниться централизованно.
.id('user')
.link('#user')
Это особенно важно в больших проектах с десятками взаимосвязанных схем.