appsync
Client-Side Integration
For Nuxt Precognition's useForm to detect validation failures and populate the form's error state, the submission callback must throw an exception when errors are present.
However, GraphQL endpoints typically return a successful 200 OK status code even when validation or execution fails, returning issues inside an errors array in the response payload.
To bridge this gap, we must intercept the GraphQL response, check if any validation errors occurred, and explicitly throw them. This is achieved by creating a custom plugin that wraps the GraphQL client (e.g. Amplify's GraphQL client) with a Proxy to intercept calls and throw custom ValidationError exceptions.
This integration is split into four main parts:
1. GraphQL Client Proxy Interceptor
We intercept the GraphQL requests (e.g., using a Proxy on AWS Amplify's mutations or queries). If the response contains errors, we pass them to a utility helper to throw custom validation errors:
// plugins/amplify.client.ts
import { generateClient } from 'aws-amplify/api'
const client = generateClient({ authMode: 'userPool' })
client.mutations = new Proxy(client.mutations, {
get(target, prop) {
if (typeof target[prop as keyof typeof prop] !== 'function') {
return target[prop as keyof typeof prop]
}
return async (...input: any[]) => {
const result = await target[prop as keyof typeof target](...input)
if (result.errors?.length) {
// Intercept and handle AppSync GraphQL errors
handleAppSyncGraphQlErrors(result.errors)
}
return result
}
}
})
2. Error Parser Utility & ValidationError Class
We parse validation errors from AppSync. This includes:
- AppSync JS resolver errors (returned with
errorType: 'ValidationError'), which we map using theValidationErrorclass andgraphQlValidationparser. - Lambda resolver errors (returned with
errorType: 'Lambda:Unhandled'), where the Lambda throws a stringified JSON exception containing Zod validation issues. We intercept this, parse it, and throw aZodErrorto be resolved by thezodErrorParser.
// utils/zodParser.ts
import * as z from 'zod'
export class ValidationError extends Error {
constructor(message: string, public issues: any[]) {
super(message)
this.name = 'ValidationError'
}
}
// 1. Parser for standard Zod Errors (e.g. from Lambdas)
export function zodErrorParser(error: Error) {
if (error instanceof z.ZodError) {
const errors = {} as Record<string, string[]>
error.issues.forEach((value) => {
const key = value.path.join('.')
if (key in errors) {
errors[key].push(value.message)
} else {
errors[key] = [value.message]
}
})
return { errors, message: 'Validation error' }
}
return null
}
// 2. Parser for AppSync JS Resolver ValidationErrors
export function graphQlValidation(error: Error) {
if (error instanceof ValidationError) {
const errors = {} as Record<string, string[]>
error.issues.forEach((issue) => {
const key = typeof issue.errorInfo?.path === 'string'
? issue.errorInfo?.path
: Array.isArray(issue.errorInfo?.path)
? issue.errorInfo.path.join('.')
: null
if (!key) return
if (key in errors) {
errors[key].push(issue.message)
} else {
errors[key] = [issue.message]
}
})
return { errors, message: 'Validation error' }
}
return null
}
// 3. Central error handler called by the client Proxy
export function handleAppSyncGraphQlErrors(errors: any[]): never {
switch (errors.at(0)?.errorType) {
case 'Lambda:Unhandled':
return handleLambdaUnhandledError(errors[0]!.message)
case 'ValidationError':
throw new ValidationError(errors.at(0)!.message, errors)
default:
throw new Error(errors.at(0)!.message)
}
}
// 4. Extracts JSON error payload from unhandled Lambda errors
function handleLambdaUnhandledError(message: string): never {
if (!message.startsWith('{') || !message.endsWith('}')) {
throw new Error(message)
}
const err = JSON.parse(message)
if (typeof err !== 'object' || err.name !== 'AppSyncError') {
throw new Error(message)
}
switch (err.errorType) {
case 'ValidationError':
return handleValidationError(err)
default:
throw new Error(err.message)
}
}
function handleValidationError(err: { message: string, errorInfo?: { issues?: z.ZodIssue[] } }): never {
if (err.errorInfo?.issues) {
throw new z.ZodError(err.errorInfo.issues)
}
throw new Error(err.message)
}
3. Registering the Custom Precognition Error Parsers
Push both zodErrorParser and graphQlValidation into Nuxt Precognition's $precognition.errorParsers array during application setup:
// plugins/precognitionParser.ts
export default defineNuxtPlugin({
name: 'PrecognitionParser',
setup() {
const { $precognition } = useNuxtApp()
$precognition.errorParsers.push(zodErrorParser, graphQlValidation)
},
})
4. Composables Form Usage
When calling useForm, forward the validation headers as HTTP request headers into the Amplify client configuration:
// composables/useCreateTaskForm.ts
import { useForm } from 'nuxt-precognition'
export default function (props: any) {
const { $Amplify } = useNuxtApp()
const form = useForm(() => ({
title: props.task?.title || '',
description: props.task?.description || '',
priority: props.task?.priority || 'NORMAL',
}), (data, headers) => {
return $Amplify.GraphQL.client.mutations.createTask({
title: data.title,
description: data.description,
priority: data.priority,
}, {
// Forward headers from useForm to the GraphQL request
headers,
})
})
return { form }
}