Validation Adapter
Validate mutations server-side with schema-based validation.
Overview
The Validation Adapter validates all mutations before they’re applied to the database, ensuring data integrity and providing clear error messages.
Quick Start
import { PrismaAdapter } from '@gluonic/server-prisma'
import { createValidationAdapter, createZodValidator } from '@gluonic/server/adapters'
import { z } from 'zod'
// 1. Define validation schemas
const validator = createZodValidator({
user: {
insert: z.object({
email: z.string().email('Invalid email'),
name: z.string().min(1, 'Name required')
}),
update: z.object({
email: z.string().email().optional(),
name: z.string().min(1).optional()
})
},
post: {
insert: z.object({
title: z.string().min(3).max(100),
content: z.string().min(10)
}),
update: z.object({
title: z.string().min(3).max(100).optional(),
content: z.string().min(10).optional()
})
}
})
// 2. Create database adapter
const dbAdapter = PrismaAdapter({ prisma, autoDiscover: { enabled: true } })
// 3. Wrap with validation
const adapter = createValidationAdapter({
adapter: dbAdapter,
validator
})
// 4. Use validated adapter
await SyncServer(app, {
adapter // Mutations validated automatically ✓
})Validation Libraries
Zod (Recommended)
import { createZodValidator } from '@gluonic/server/adapters'
import { z } from 'zod'
const validator = createZodValidator({
post: {
insert: z.object({
title: z.string().min(1).max(200),
content: z.string(),
published: z.boolean().default(false)
}),
update: z.object({
title: z.string().min(1).max(200).optional(),
content: z.string().optional(),
published: z.boolean().optional()
}),
delete: z.object({
// Can add delete validation too
reason: z.string().optional()
})
}
})Joi
import { createJoiValidator } from '@gluonic/server/adapters'
import Joi from 'joi'
const validator = createJoiValidator({
user: {
insert: Joi.object({
email: Joi.string().email().required(),
name: Joi.string().min(1).required()
}),
update: Joi.object({
email: Joi.string().email(),
name: Joi.string().min(1)
})
}
})Custom Validator
import { createCustomValidator } from '@gluonic/server/adapters'
const validator = createCustomValidator({
validate: async (model, operation, data, context) => {
if (model === 'post' && operation === 'insert') {
if (!data.title || data.title.length < 3) {
throw new ValidationError('Title must be at least 3 characters')
}
// Custom business logic
if (data.published && !data.content) {
throw new ValidationError('Cannot publish empty post')
}
}
return true
}
})Error Handling
Validation Errors
// Client sends invalid mutation
POST /sync/v1/tx {
ops: [{
t: 'post',
id: 'p1',
op: 'i',
patch: {
title: 'ab', // Too short!
content: 'x' // Too short!
}
}]
}
// Server validates and rejects
{
ok: true,
results: [{
ok: false,
error: 'validation_failed',
details: {
title: ['String must contain at least 3 character(s)'],
content: ['String must contain at least 10 character(s)']
}
}]
}
// Client receives error
// Optimistic update rolls back
// User notified with specific errors ✓Client Handling
try {
await store.save('post', id, { title: 'ab' })
} catch (error) {
if (error.code === 'validation_failed') {
// Show field-specific errors
for (const [field, errors] of Object.entries(error.details)) {
showError(field, errors[0])
}
}
}Per-Field Validation
const validator = createZodValidator({
post: {
insert: z.object({
title: z.string()
.min(3, 'Title too short')
.max(100, 'Title too long')
.regex(/^[a-zA-Z0-9\s]+$/, 'Title contains invalid characters'),
content: z.string()
.min(10, 'Content too short')
.max(50000, 'Content too long'),
tags: z.array(z.string())
.max(10, 'Maximum 10 tags')
.optional(),
published: z.boolean()
})
}
})Context-Aware Validation
Access request context in validation:
const validator = createCustomValidator({
validate: async (model, operation, data, context) => {
const { userId, userRole, orgId } = context
if (model === 'post' && operation === 'update') {
// Only admins can publish
if (data.published && userRole !== 'admin') {
throw new ValidationError('Only admins can publish posts')
}
// Check user owns the post
const post = await prisma.post.findUnique({ where: { id: data.id } })
if (post.authorId !== userId) {
throw new ValidationError('Cannot edit other users posts')
}
}
}
})Conditional Validation
Different rules based on state:
const validator = createCustomValidator({
validate: async (model, operation, data, context) => {
if (model === 'post') {
if (operation === 'insert') {
// Creating: title required
if (!data.title) throw new ValidationError('Title required')
}
if (operation === 'update' && data.published === true) {
// Publishing: content must be substantial
const post = await prisma.post.findUnique({ where: { id: data.id } })
if (!post.content || post.content.length < 100) {
throw new ValidationError('Cannot publish posts with less than 100 characters')
}
}
}
}
})Client-Side Pre-Validation
Validate before sending:
import { createValidator } from '@gluonic/client/helpers'
const clientValidator = createValidator({
post: {
title: (val) => {
if (val.length < 3) return 'Title must be at least 3 characters'
if (val.length > 100) return 'Title must be less than 100 characters'
return null
},
content: (val) => {
if (val.length < 10) return 'Content too short'
return null
}
}
})
// In component
const handleSave = async () => {
const errors = clientValidator.validate('post', { title, content })
if (errors) {
showErrors(errors)
return
}
await store.save('post', id, { title, content })
}Next Steps
- Authorization Adapter - Permission checks
- Caching Adapter - Performance
- Custom Adapters - Build your own
Last updated on