Skip to Content
AdvancedValidation Adapter

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

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

Last updated on