npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

nuxt-precognition

v0.0.6

Published

Nuxt module implementing Precognition protocol (inspired by official Laravel Precognition) for backend precognitive validation.

Downloads

132

Readme

Nuxt Precognition

npm version npm downloads License Nuxt

This is a new version of nuxt-laravel-precognition. It offers same features, but being not dependent on Laravel.

Instead of supporting only $fetch and Laravel, it works with simple promises, targeting any backend that implements the base Precognition protocol. These promises will receive the form payload and protocol Headers.

Example

interface User = {
  email: string
  password: string
}

const form = useForm(
  (): User => ({ email: '', password: '' }),
  (body, headers) => $fetch('/api/login', { method: 'POST', headers, body })
)

This module comes with native Nitro integration, but will work with other backend as well.

Are you using only Lambda? You are covered will Lambda Precognition!!

It supports any validation library (who said Zod??) server or client side. You will need only to configure specific Error parsers.

Features

  •  Laravel compliant
  •  Validation library agnostic
  •  Client and server side validation
  •  Optimal Typescript support
  •  Highly customizable

How it works

Everything turns around errorParsers(user defined function to read validation errors from Error payload):

type ValidationErrors = Record<string, string | string[]>

interface ValidationErrorsData {
  message: string
  errors: ValidationErrors
}

type ValidationErrorParser = (error: Error) => ValidationErrorsData | undefined | null

You can define them globally (in Nuxt Plugin or custom eventHandler), or per form instance.

Imagine you are working with Zod.
Just create a nuxt plugin and define the "Zod error parser":

// plugins/precognition.ts

export default defineNuxtPlugin(() => {
  const { $precognition } = useNuxtApp()

  $precognition.errorParsers.push(
    (error) => {
      if (error instanceof ZodError) {
        const errors = {} as Record<string, string[]>
        error.errors.forEach((e) => {
          const key = e.path.join('.')
          if (key in errors) {
            errors[key].push(e.message)
            return
          }
          errors[key] = [e.message]
        })
        return { errors, message: 'Validation error' }
      }
      return null
    },
  )
})

From now on, everytime the useForm will catch the error, it will run our parses, and capture and assign any validation errors.

If you want to reuse the same options over multiple pages, you can create your custom composable by useForm.create factory function.

How about server side

Same idea, creating a nitro plugin:

// server/plugins/precognition.ts

import { ZodError } from 'zod'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', (event) => {
    event.context.$precognition.errorParsers = [
      (error) => {
        if (error instanceof ZodError) {
          const errors: Record<string, string[]> = {}
          error.errors.forEach((e) => {
            const key = e.path.join('.')
            if (key in errors) {
              errors[key].push(e.message)
              return
            }
            errors[key] = [e.message]
          })
          const message = error.errors.at(0)?.message ?? 'Validation error'
          return { errors, message }
        }
      },
    ]
  })
})

If you don't like hooking on every request, you can create your custom eventHandler by definePrecognitiveEventHandler.create factory function.

Make your validation logic inside the onRequest handler of the definePrecognitiveEventHandler.

// server/api/login.post.ts
import { z } from 'zod'
import { definePrecognitiveEventHandler, readBody } from '#imports'

const loginSchema = z.object({
  email: z.string().email().refine(_email => // Check for email uniqueness
    true, { message: 'Email is already in use' },
  ),
  password: z.string(),
}).refine((_data) => {
  // Check for email and password match
  // ...
  return true
},
{ message: 'invalid credentials', path: ['email'] },
)

export default definePrecognitiveEventHandler({
  async onRequest(event) {
    const body = await readBody(event)
    loginSchema.parse(body)
  },
  handler: () => {
    return {
      status: 200,
      body: {
        message: 'Success',
      },
    }
  },
})

This time the error will be converted to NuxtServerValidationError and captured client side, if we enable the predefined parsers in the nuxt configuration file:

// nuxt.config.ts

export default defineNuxtConfig({
  modules: ['nuxt-precognitiion'],
  precognition: {
    backendValidation: true,
    enableNuxtClientErrorParser: true,
  }
})

Remember to throw the ValidationError only in the onRequest handler (using the object notation).
Any logic in the base handler won't be process during precognitiveRequests.

  • Each event.context include also a flag ({ precognitive: boolean }), indicating if request is precognitive or not, looking at presence of Precognitive header.

Precognition Protocol

In case you need to define your own backend logic outside nitro (AWS Lamba), respect following list of requirements.

  • Precognitive Requests must have:
    1. Precognitive Header { 'Precognitive': 'true' }
  • To validate specific variables, each keys must be specified inside the ValidateOnly Header, comma separated and leveraging dot notation { 'Precognition-Validate-Only': 'name,age,address.street,address.number' }
  • To validate the full Form the ValidateOnly Header should be omitted or define as an empty string.
  • Successfull validation response must have:
    1. Precognitive Header { 'Precognitive': 'true' }
    2. Precognitive Successfull Header { 'Precognition-Success': 'true' }
    3. Precognitve Successfull status code: 204
  • Error validation response must have:
    1. Precognitive Header { 'Precognitive': 'true' }
    2. ValidationOnly header if needed { 'Precognition-Validate-Only': 'name,age,address.street,address.number' }
    3. Validation Error status code: 422
    4. Validation Errors and Message will be parsed as per your define logic, or using standard errorParsers:
      • NuxtErrorParsers: NuxtPrecognitiveErrorResponse: Response & { _data: { data: ValidationErrorsData }}
      • LaravelErrorParsers: LaravelPrecognitiveErrorResponse: Response & { _data: ValidationErrorsData }

Quick Setup

Install the module to your Nuxt application with one command:

npx nuxi module add nuxt-precognition

Configure

| name | type | default | description | |---|---|---|---| |validationTimeout|number|1500|Debounce time, in milliseconds, between two precognitive validation requests.| |backendValidation|boolean|false|Flag to enable the precognitive validation.| |validateFiles|boolean|false|Flag to enable files validation on precognitive requests.| |enableNuxtClientErrorParser|boolean|false|Flag to enable nuxtErrorParsers @ client side (in form.validate and form.submit).| |enableLaravelClientErrorParser|boolean|false|Flag to enable laravelErrorParsers @ client side (in form.validate and form.submit).| |enableLaravelServerErrorParser|boolean|false|Flag to enable laravelErrorParsers @ client side (in definePrecognitiveEventHandler).|

Status Handlers

Like in official package, you can define globally, or @instance level, custom handlers for specific error codes:

// plugins/precognition.ts

export default defineNuxtPlugin(() => {
  const { $precognition } = useNuxtApp()

  $precognition.statusHandlers = {
    401: async (error, form) => {
      form.error = createError('Unauthorized')
      await navigateTo('/login')
    },
    403: async (error, form) => {
      form.error = createError('Forbidden')
    },
  }
})

That's it! You can now use Nuxt Precognition in your Nuxt app ✨

Working with Laravel

  1. Define a plugin like this
// plugins/api.ts

export default defineNuxtPlugin((app) => {
  const { $precognition } = useNuxtApp()
  const token = useCookie('XSRF-TOKEN')

  const api = $fetch.create({
    baseURL: 'http://localhost',
    credentials: 'include',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    onRequest: ({ options }) => {
      // Setup csrf protection for every requests if available
      if (token.value) {
        const headers = new Headers(options.headers)
        headers.set('X-XSRF-TOKEN', token.value)
        options.headers = headers
      }
    },
    onResponse: (context) => {
      // ensure that all precognitive requests will receive precognitive responses
      $precognition.assertSuccessfulPrecognitiveResponses(context)
    },
  })

  async function fetchSanctumToken() {
    try {
      await api('/sanctum/csrf-cookie')
      token.value = useCookie('XSRF-TOKEN').value

      if (!token.value) {
        throw new Error('Failed to get CSRF token')
      }
    }
    catch (e) {
      console.error(e)
    }
  }

  app.hook('app:mounted', fetchSanctumToken)

  return {
    provide: {
      api,
      sanctum: {
        fetchToken: fetchSanctumToken,
        token,
      },
    },
  }
})
  1. Enable backend validation and native Laravel Error parsers client or server side
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-precognition'],
  precognition: {
    backendValidation: true,
    enableLaravelClientErrorParser: true,
  },
  /*
  ...
  */
})

* If you enableLaravelServerErrorParser, you must also enableNuxtClientErrorParser

  1. Setup Laravel Cors configuration file
// config/cors.php

return [

    /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    */

    'paths' => ['*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [env('FRONTEND_URL', 'http://localhost:3000')],

    'allowed_headers' => ['*'],

    'exposed_headers' => ['Precognition', 'Precognition-Success'],

    'max_age' => 0,

    'supports_credentials' => true,

];
  1. Enable the Precognition Middleware where needed
// routes/api.php

Route::middleware('precognitive')->group(function () {
    Route::apiResource('posts', \App\Http\Controllers\PostController::class);
});

Contribution

# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with the playground
npm run dev

# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release new version
npm run release