diff --git a/src/module.ts b/src/module.ts index dd54b30..56d06eb 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,19 +1,19 @@ -import { defineNuxtModule, createResolver, addImportsDir, addServerImportsDir } from '@nuxt/kit' +import { defineNuxtModule, createResolver, addImportsDir, addServerImportsDir } from "@nuxt/kit"; // Module options TypeScript interface definition export interface ModuleOptions {} export default defineNuxtModule({ meta: { - name: 'nuxt-precognition', - configKey: 'nuxtPrecognition', + name: "nuxt-precognition", + configKey: "nuxtPrecognition", }, // Default configuration options of the Nuxt module defaults: {}, setup(_options, _nuxt) { - const resolver = createResolver(import.meta.url) + const resolver = createResolver(import.meta.url); - addImportsDir(resolver.resolve('./runtime/composables')) - addServerImportsDir(resolver.resolve('./runtime/server/utils')) + addImportsDir(resolver.resolve("./runtime/composables")); + addServerImportsDir(resolver.resolve("./runtime/server/utils")); }, -}) +}); diff --git a/src/runtime/composables/useForm.ts b/src/runtime/composables/useForm.ts index 9c6120a..33ec144 100644 --- a/src/runtime/composables/useForm.ts +++ b/src/runtime/composables/useForm.ts @@ -1,54 +1,52 @@ -import isEqual from 'lodash-es/isequal' -import { reactive, watch } from 'vue' -import type { NitroFetchRequest } from 'nitropack' -import type FormDataConvertible from '~/types/FormDataconvertible' -import type Method from '~/types/Method' -import type VisitOptions from '~/types/VisitOptions' +import isEqual from "lodash-es/isEqual"; +import { reactive, watch } from "vue"; +import type { NitroFetchRequest } from "nitropack"; +import type FormDataConvertible from "~/types/FormDataconvertible"; +import type Method from "~/types/Method"; +import type VisitOptions from "~/types/VisitOptions"; -type FormDataType = object +type FormDataType = object; interface InertiaFormProps { - isDirty: boolean - errors: Partial> - hasErrors: boolean - processing: boolean - wasSuccessful: boolean - recentlySuccessful: boolean - data(): TForm - transform(callback: (data: TForm) => object): this - defaults(field: keyof TForm, value: FormDataConvertible): this - defaults(fields?: Partial): this - reset(...fields: (keyof TForm)[]): this - clearErrors(...fields: (keyof TForm)[]): this - setError(field: keyof TForm, value: string): this - setError(errors: Record): this - submit(method: Method, url: string, options?: Partial): Promise - get(url: string, options?: Partial): void - post(url: string, options?: Partial): Promise - put(url: string, options?: Partial): void - patch(url: string, options?: Partial): void - delete(url: string, options?: Partial): void + isDirty: boolean; + errors: Partial>; + hasErrors: boolean; + processing: boolean; + wasSuccessful: boolean; + recentlySuccessful: boolean; + data(): TForm; + transform(callback: (data: TForm) => object): this; + defaults(field: keyof TForm, value: FormDataConvertible): this; + defaults(fields?: Partial): this; + reset(...fields: (keyof TForm)[]): this; + clearErrors(...fields: (keyof TForm)[]): this; + setError(field: keyof TForm, value: string): this; + setError(errors: Record): this; + submit(method: Method, url: string, options?: Partial): Promise; + get(url: string, options?: Partial): void; + post(url: string, options?: Partial): Promise; + put(url: string, options?: Partial): void; + patch(url: string, options?: Partial): void; + delete(url: string, options?: Partial): void; } -export type InertiaForm = TForm & InertiaFormProps +export type InertiaForm = TForm & InertiaFormProps; -export default function useForm(data: TForm | (() => TForm)): InertiaForm +export default function useForm(data: TForm | (() => TForm)): InertiaForm; export default function useForm( rememberKey: string, data: TForm | (() => TForm), -): InertiaForm +): InertiaForm; export default function useForm( rememberKeyOrData: string | TForm | (() => TForm), maybeData?: TForm | (() => TForm), ): InertiaForm { - const rememberKey = typeof rememberKeyOrData === 'string' ? rememberKeyOrData : null - const data = typeof rememberKeyOrData === 'string' ? maybeData : rememberKeyOrData - const restored = rememberKey - ? (restore(rememberKey) as { data: TForm, errors: Record }) - : null - let defaults = typeof data === 'object' ? structuredClone(data) : structuredClone(data()) - let recentlySuccessfulTimeoutId = null - let _transform = data => data + const rememberKey = typeof rememberKeyOrData === "string" ? rememberKeyOrData : null; + const data = typeof rememberKeyOrData === "string" ? maybeData : rememberKeyOrData; + const restored = rememberKey ? (restore(rememberKey) as { data: TForm; errors: Record }) : null; + let defaults = typeof data === "object" ? structuredClone(data) : structuredClone(data()); + let recentlySuccessfulTimeoutId = null; + let _transform = (data) => data; const form = reactive({ ...(restored ? restored.data : structuredClone(defaults)), @@ -61,56 +59,54 @@ export default function useForm( recentlySuccessful: false, data() { return (Object.keys(defaults) as Array).reduce((carry, key) => { - carry[key] = this[key] - return carry - }, {} as Partial) as TForm + carry[key] = this[key]; + return carry; + }, {} as Partial) as TForm; }, transform(callback) { - _transform = callback - return this + _transform = callback; + return this; }, defaults(fieldOrFields?: keyof TForm | Partial, maybeValue?: FormDataConvertible) { - if (typeof data === 'function') { - throw new TypeError('You cannot call `defaults()` when using a function to define your form data.') + if (typeof data === "function") { + throw new TypeError("You cannot call `defaults()` when using a function to define your form data."); } - if (typeof fieldOrFields === 'undefined') { - defaults = this.data() - } - else { + if (typeof fieldOrFields === "undefined") { + defaults = this.data(); + } else { defaults = Object.assign( {}, structuredClone(defaults), - typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields, - ) + typeof fieldOrFields === "string" ? { [fieldOrFields]: maybeValue } : fieldOrFields, + ); } - return this + return this; }, reset(...fields) { - const resolvedData = typeof data === 'object' ? structuredClone(defaults) : structuredClone(data()) - const clonedData = structuredClone(resolvedData) + const resolvedData = typeof data === "object" ? structuredClone(defaults) : structuredClone(data()); + const clonedData = structuredClone(resolvedData); if (fields.length === 0) { - defaults = clonedData - Object.assign(this, resolvedData) - } - else { + defaults = clonedData; + Object.assign(this, resolvedData); + } else { Object.keys(resolvedData) - .filter(key => fields.includes(key)) + .filter((key) => fields.includes(key)) .forEach((key) => { - defaults[key] = clonedData[key] - this[key] = resolvedData[key] - }) + defaults[key] = clonedData[key]; + this[key] = resolvedData[key]; + }); } - return this + return this; }, setError(fieldOrFields: keyof TForm | Record, maybeValue?: string) { - Object.assign(this.errors, typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields) + Object.assign(this.errors, typeof fieldOrFields === "string" ? { [fieldOrFields]: maybeValue } : fieldOrFields); - this.hasErrors = Object.keys(this.errors).length > 0 + this.hasErrors = Object.keys(this.errors).length > 0; - return + return; }, clearErrors(...fields) { this.errors = Object.keys(this.errors).reduce( @@ -119,159 +115,158 @@ export default function useForm( ...(fields.length > 0 && !fields.includes(field) ? { [field]: this.errors[field] } : {}), }), {}, - ) + ); - this.hasErrors = Object.keys(this.errors).length > 0 + this.hasErrors = Object.keys(this.errors).length > 0; - return this + return this; }, async submit(method, url, options: VisitOptions = {}) { - const data = this.transform(this.data()) + const data = this.transform(this.data()); const _options = { ...options, onBefore: () => { - console.log('onBefore') - this.wasSuccessful = false - this.recentlySuccessful = false - clearTimeout(recentlySuccessfulTimeoutId) + console.log("onBefore"); + this.wasSuccessful = false; + this.recentlySuccessful = false; + clearTimeout(recentlySuccessfulTimeoutId); if (options.onBefore) { - return options.onBefore() + return options.onBefore(); } }, onStart: (visit) => { - console.log('onStart', visit) - this.processing = true + console.log("onStart", visit); + this.processing = true; if (options.onStart) { - return options.onStart(visit) + return options.onStart(visit); } }, onProgress: (event) => { - this.progress = event + this.progress = event; if (options.onProgress) { - return options.onProgress(event) + return options.onProgress(event); } }, onSuccess: async (response) => { - console.log('onSuccess', response) - this.processing = false - this.progress = null - this.clearErrors() - this.wasSuccessful = true - this.recentlySuccessful = true - recentlySuccessfulTimeoutId = setTimeout(() => (this.recentlySuccessful = false), 2000) + console.log("onSuccess", response); + this.processing = false; + this.progress = null; + this.clearErrors(); + this.wasSuccessful = true; + this.recentlySuccessful = true; + recentlySuccessfulTimeoutId = setTimeout(() => (this.recentlySuccessful = false), 2000); - const onSuccess = options.onSuccess ? await options.onSuccess(response) : null - defaults = structuredClone(this.data()) - this.isDirty = false - return onSuccess + const onSuccess = options.onSuccess ? await options.onSuccess(response) : null; + defaults = structuredClone(this.data()); + this.isDirty = false; + return onSuccess; }, onError: (errors) => { - console.log('onError', errors) - this.processing = false - this.progress = null - this.clearErrors().setError(errors) + console.log("onError", errors); + this.processing = false; + this.progress = null; + this.clearErrors().setError(errors); if (options.onError) { - return options.onError(errors) + return options.onError(errors); } }, onFinish: (visit) => { - console.log('onFinish') - this.processing = false - this.progress = null + console.log("onFinish"); + this.processing = false; + this.progress = null; if (options.onFinish) { - return options.onFinish(visit) + return options.onFinish(visit); } }, - } + }; // run before hook - const beforeResult = _options.onBefore() + const beforeResult = _options.onBefore(); if (beforeResult === false) { - return + return; } - let response + let response; try { response = await $fetch(url, { method: method, body: data, onRequest: async ({ request, options }) => { - console.log('oFetch onRequest', request, options) + console.log("oFetch onRequest", request, options); - await _options.onStart() + await _options.onStart(); }, onResponse: async ({ response }) => { - console.log('onResponse') + console.log("onResponse"); // onResponse is always called, even if there was an errors // return early so we don't execute both this and onResponseError if (!response.ok) { - return + return; } - await _options.onSuccess(response) - await _options.onFinish() + await _options.onSuccess(response); + await _options.onFinish(); }, async onResponseError({ response }) { - console.log('onResponseError') - const errors = response._data.data?.errors - await _options.onError(errors) - await _options.onFinish() + console.log("onResponseError"); + const errors = response._data.data?.errors; + await _options.onError(errors); + await _options.onFinish(); }, - }) - } - catch (e) { + }); + } catch (e) { // we don't need to do anything here, the onError hook will handle it } - return response + return response; }, get(url, options) { - this.submit('get', url, options) + this.submit("get", url, options); }, post(url, options) { - return this.submit('post', url, options) + return this.submit("post", url, options); }, put(url, options) { - this.submit('put', url, options) + this.submit("put", url, options); }, patch(url, options) { - this.submit('patch', url, options) + this.submit("patch", url, options); }, delete(url, options) { - this.submit('delete', url, options) + this.submit("delete", url, options); }, __rememberable: rememberKey === null, __remember() { - return { data: this.data(), errors: this.errors } + return { data: this.data(), errors: this.errors }; }, __restore(restored) { - Object.assign(this, restored.data) - this.setError(restored.errors) + Object.assign(this, restored.data); + this.setError(restored.errors); }, - }) + }); watch( form, (newValue) => { - form.isDirty = !isEqual(form.data(), defaults) + form.isDirty = !isEqual(form.data(), defaults); if (rememberKey) { - router.remember(structuredClone(newValue.__remember()), rememberKey) + router.remember(structuredClone(newValue.__remember()), rememberKey); } }, { immediate: true, deep: true }, - ) + ); - return form + return form; } -function restore(key = 'default'): unknown { +function restore(key = "default"): unknown { if (import.meta.server) { - return + return; } - return window.history.state?.rememberedState?.[key] + return window.history.state?.rememberedState?.[key]; } diff --git a/src/runtime/composables/usePrecognitionForm.ts b/src/runtime/composables/usePrecognitionForm.ts index 9f5cdcb..0efbc38 100644 --- a/src/runtime/composables/usePrecognitionForm.ts +++ b/src/runtime/composables/usePrecognitionForm.ts @@ -1,100 +1,100 @@ -import type { FetchContext, FetchResponse } from 'ofetch' -import useForm from './useForm' +import type { FetchContext, FetchResponse } from "ofetch"; +import useForm from "./useForm"; -export default function usePrecognitionForm>(method: RequestMethod, url: string | (() => string), data: Data) { - const form = useForm(data) - form.method = method - form.url = resolveUrl(url) - form.validate = validate - form.validating = false - const baseSubmit = form.submit +export default function usePrecognitionForm>( + method: RequestMethod, + url: string | (() => string), + data: Data, +) { + const form = useForm(data); + form.method = method; + form.url = resolveUrl(url); + form.validate = validate; + form.validating = false; + const baseSubmit = form.submit; const precogSubmit = (data: Record) => { - return baseSubmit.bind(form)(form.method, form.url, data) - } + return baseSubmit.bind(form)(form.method, form.url, data); + }; - form.submit = precogSubmit.bind(form) + form.submit = precogSubmit.bind(form); - return form + return form; } async function validate(fieldName: string) { - console.log('validate', fieldName) + console.log("validate", fieldName); // check if the fieldName is an array - let onlyFieldsToValidate - const transformedData = this.transform(this.data()) + let onlyFieldsToValidate; + const transformedData = this.transform(this.data()); if (Array.isArray(fieldName)) { // get only the keys that are listed in the array fieldName.forEach((key) => { if (onlyFieldsToValidate[key] !== undefined) { - onlyFieldsToValidate[key] = transformedData[key] + onlyFieldsToValidate[key] = transformedData[key]; } - }) - } - else { - onlyFieldsToValidate = { [fieldName]: transformedData[fieldName] } + }); + } else { + onlyFieldsToValidate = { [fieldName]: transformedData[fieldName] }; } const defaultOptions = { onStart: () => { - console.log('precognition onStart') - this.validating = true + console.log("precognition onStart"); + this.validating = true; }, onSuccess: async (response) => { - console.log('precognition onSuccess', response) - this.clearErrors() + console.log("precognition onSuccess", response); + this.clearErrors(); }, onError: (errors) => { - console.log('precognitionOnError', errors) - this.setError(errors) + console.log("precognitionOnError", errors); + this.setError(errors); }, onFinish: () => { - console.log('precognitionOnFinish') - this.validating = false + console.log("precognitionOnFinish"); + this.validating = false; }, - } + }; - const validateOnly = Object.keys(onlyFieldsToValidate).join() + const validateOnly = Object.keys(onlyFieldsToValidate).join(); - console.log('precognition oFetch') + console.log("precognition oFetch"); try { await $fetch(this.url, { method: this.method, headers: { - 'Precognition': true, - 'Precognition-Validate-Only': validateOnly, + Precognition: true, + "Precognition-Validate-Only": validateOnly, }, body: onlyFieldsToValidate, onRequest: async ({ request, options }) => { - console.log('precognition oFetch onRequest', request, options) + console.log("precognition oFetch onRequest", request, options); - await defaultOptions.onStart() + await defaultOptions.onStart(); }, onResponse: async (context: FetchContext & { response: FetchResponse }): Promise | void => { if (!context.response.ok) { - return + return; } // clear the errors for the validated fields - await this.clearErrors(validateOnly) + await this.clearErrors(validateOnly); }, onResponseError: async ({ response }) => { - console.log('precognition onResponseError') - const errors = response._data.data?.errors - await defaultOptions.onError(errors) - await defaultOptions.onFinish() + console.log("precognition onResponseError"); + const errors = response._data.data?.errors; + await defaultOptions.onError(errors); + await defaultOptions.onFinish(); }, - }) - } - catch (e) { + }); + } catch (e) { if (e.status !== 422) { // this isn't a precognition error - throw e + throw e; } } } -type RequestMethod = 'get' | 'post' | 'patch' | 'put' | 'delete' +type RequestMethod = "get" | "post" | "patch" | "put" | "delete"; -const resolveUrl = (url: string | (() => string)): string => typeof url === 'string' - ? url - : url() +const resolveUrl = (url: string | (() => string)): string => (typeof url === "string" ? url : url()); diff --git a/src/runtime/server/utils/definePrecognitionEventHandler.ts b/src/runtime/server/utils/definePrecognitionEventHandler.ts index d939fcb..1676062 100644 --- a/src/runtime/server/utils/definePrecognitionEventHandler.ts +++ b/src/runtime/server/utils/definePrecognitionEventHandler.ts @@ -1,61 +1,61 @@ -import { type EventHandler, type EventHandlerRequest, type H3Event, setResponseHeader } from 'h3' -import type { ZodSchema, ZodObject } from 'zod' -import { z } from 'zod' -import { defineEventHandler, getHeaders } from 'h3' -import getValidatedInput from './getValidatedInput' +import { type EventHandler, type EventHandlerRequest, type H3Event, setResponseHeader } from "h3"; +import type { ZodSchema, ZodObject } from "zod"; +import { z } from "zod"; +import { defineEventHandler, getHeaders } from "h3"; +import getValidatedInput from "./getValidatedInput"; -const precognitionEventHandler = ( +const precognitionEventHandler = ( zodObject: ZodObject, handler: EventHandler, ): EventHandler => - defineEventHandler(async (event) => { - // do something before the route handler - console.log('starting precognition event handler') - const headers = getHeaders(event) + defineEventHandler(async (event) => { + // do something before the route handler + console.log("starting precognition event handler"); + const headers = getHeaders(event); - if (!headers.precognition) { - // this is not a precognition event - // return the regular response - console.log('Regular event handler running') + if (!headers.precognition) { + // this is not a precognition event + // return the regular response + console.log("Regular event handler running"); - return handler(event) - } + return handler(event); + } - const validateOnlyHeader = headers['precognition-validate-only'] - const fieldsToValidate = validateOnlyHeader - ? validateOnlyHeader.split(',') - : [] + const validateOnlyHeader = headers["precognition-validate-only"]; + const fieldsToValidate = validateOnlyHeader ? validateOnlyHeader.split(",") : []; - // this is a precognition event - console.log('Handling precognition event...') - return await processPrecognitionRequest(event, zodObject, fieldsToValidate) - }) + // this is a precognition event + console.log("Handling precognition event..."); + return await processPrecognitionRequest(event, zodObject, fieldsToValidate); + }); async function processPrecognitionRequest(event: H3Event, zodSchema: ZodSchema, fieldsToValidate: string) { // get the field we want to validate from the precognition object // get the zod validation schema only for the fields we want to validate - const zodSchemaToUse = fieldsToValidate.reduce((obj, field) => { - obj[field] = zodSchema.shape[field] - return obj - }, {} as Record) + const zodSchemaToUse = fieldsToValidate.reduce( + (obj, field) => { + obj[field] = zodSchema.shape[field]; + return obj; + }, + {} as Record, + ); // turn our individual schema into a zod object - const schema = z.object (zodSchemaToUse) + const schema = z.object(zodSchemaToUse); // validate just this one field try { - await getValidatedInput(event, schema) - } - catch (error) { + await getValidatedInput(event, schema); + } catch (error) { // only handle our expected errors if (!(error.statusCode === 422)) { - throw error + throw error; } - setResponseHeader(event, 'precognition', true) - throw error + setResponseHeader(event, "precognition", true); + throw error; } - return true + return true; } -export default precognitionEventHandler +export default precognitionEventHandler; diff --git a/src/runtime/server/utils/getValidatedInput.ts b/src/runtime/server/utils/getValidatedInput.ts index f14fa52..c5af906 100644 --- a/src/runtime/server/utils/getValidatedInput.ts +++ b/src/runtime/server/utils/getValidatedInput.ts @@ -1,25 +1,23 @@ -import { type H3Event } from 'h3' -import type { ZodObject } from 'zod' -import { readValidatedBody, createError } from 'h3' +import { type H3Event } from "h3"; +import type { ZodObject } from "zod"; +import { readValidatedBody, createError } from "h3"; -export default async function(event: H3Event, validationSchema: ZodObject) { - const body = await readValidatedBody(event, body => validationSchema.safeParse(body)) +export default async function (event: H3Event, validationSchema: ZodObject) { + const body = await readValidatedBody(event, (body) => validationSchema.safeParse(body)); if (!body.success) { // there was an error validating the body - const fieldErrors = body.error.flatten().fieldErrors - throwValidationError(fieldErrors) + const fieldErrors = body.error.flatten().fieldErrors; + throwValidationError(fieldErrors); } - return body.data + return body.data; } function throwValidationError(errors: Record) { - const error = createError( - { - statusCode: 422, - message: 'Validation Error', - data: { errors }, - }, - ) - throw error + const error = createError({ + statusCode: 422, + message: "Validation Error", + data: { errors }, + }); + throw error; } diff --git a/src/runtime/server/utils/precognitionEventHandler.ts b/src/runtime/server/utils/precognitionEventHandler.ts index a925046..6bb9d55 100644 --- a/src/runtime/server/utils/precognitionEventHandler.ts +++ b/src/runtime/server/utils/precognitionEventHandler.ts @@ -1,3 +1,3 @@ -import precognitionEventHandler from './definePrecognitionEventHandler' +import precognitionEventHandler from "./definePrecognitionEventHandler"; -export default precognitionEventHandler +export default precognitionEventHandler; diff --git a/src/types/FormDataConvertible.d.ts b/src/types/FormDataConvertible.d.ts index e4e9366..9454c46 100644 --- a/src/types/FormDataConvertible.d.ts +++ b/src/types/FormDataConvertible.d.ts @@ -7,4 +7,4 @@ export type FormDataConvertible = | boolean | number | null - | undefined + | undefined; diff --git a/src/types/Method.d.ts b/src/types/Method.d.ts index 642ec8c..cc35a6a 100644 --- a/src/types/Method.d.ts +++ b/src/types/Method.d.ts @@ -1 +1 @@ -export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' +export type Method = "get" | "post" | "put" | "patch" | "delete"; diff --git a/src/types/VisitOptions.d.ts b/src/types/VisitOptions.d.ts index 134cd30..ca2512f 100644 --- a/src/types/VisitOptions.d.ts +++ b/src/types/VisitOptions.d.ts @@ -1,49 +1,49 @@ export type GlobalEventsMap = { before: { - parameters: [PendingVisit] + parameters: [PendingVisit]; details: { - visit: PendingVisit - } - result?: boolean - } + visit: PendingVisit; + }; + result?: boolean; + }; start: { - parameters: [PendingVisit] + parameters: [PendingVisit]; details: { - visit: PendingVisit - } - } + visit: PendingVisit; + }; + }; finish: { - parameters: [ActiveVisit] + parameters: [ActiveVisit]; details: { - visit: ActiveVisit - } - } + visit: ActiveVisit; + }; + }; success: { - parameters: [Page] + parameters: [Page]; details: { - page: Page - } - } + page: Page; + }; + }; error: { - parameters: [Errors] + parameters: [Errors]; details: { - errors: Errors - } - } -} + errors: Errors; + }; + }; +}; -export type GlobalEventNames = keyof GlobalEventsMap +export type GlobalEventNames = keyof GlobalEventsMap; export type GlobalEventCallback = ( ...params: GlobalEventParameters -) => GlobalEventResult +) => GlobalEventResult; export type VisitOptions = Partial< Visit & { - onBefore: GlobalEventCallback<'before'> - onStart: GlobalEventCallback<'start'> - onFinish: GlobalEventCallback<'finish'> - onSuccess: GlobalEventCallback<'success'> - onError: GlobalEventCallback<'error'> + onBefore: GlobalEventCallback<"before">; + onStart: GlobalEventCallback<"start">; + onFinish: GlobalEventCallback<"finish">; + onSuccess: GlobalEventCallback<"success">; + onError: GlobalEventCallback<"error">; } -> +>;