From 01b63e441f0a37dd46e47bd8206dd5e833b8b59c Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Wed, 6 Nov 2024 14:51:15 -0500 Subject: [PATCH] feat: add runtime hooks for API request and response handling --- docs/guide/hooks.md | 21 ++++++++++++++++++++- src/module.ts | 14 ++++++++++++++ src/runtime/composables/$api.ts | 12 ++++++++++++ src/runtime/composables/tsconfig.json | 3 +++ src/runtime/composables/useApiData.ts | 22 ++++++++++++++++++---- src/runtime/hooks.ts | 21 +++++++++++++++++++++ src/runtime/server/$api.ts | 13 ++++++++++++- src/runtime/server/handler.ts | 10 +++++++++- src/runtime/server/tsconfig.json | 3 +++ 9 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 src/runtime/composables/tsconfig.json create mode 100644 src/runtime/hooks.ts create mode 100644 src/runtime/server/tsconfig.json diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index dcbdaf2..2c74416 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -10,7 +10,7 @@ For more information on how to work with hooks, see the [Nuxt documentation](htt | ---------- | --------- | ----------- | | `api-party:extend` | `options` | Called during module initialization after the options have been resolved. Can be used to modify the endpoint configuration. | -## Usage +### Usage To use hooks, define them in the `hooks` property of your `nuxt.config.ts` file. The following example demonstrates how to use the `api-party:extend` hook: @@ -26,3 +26,22 @@ export default defineNuxtConfig({ }, }) ``` + +## Nuxt Runtime Hooks + +Register these hooks with a client plugin. + +| Hook name | Arguments | Description +| -------------------- | ---------- | ----------- +| `api-party:request` | `ctx` | Called before each request is made. Can be used to log or modify the request. +| `api-party:response` | `ctx` | Called after each request is made. Can be used to log or modify the response. + +## Nitro Runtime Hooks + +Register these hooks with a server plugin. + +| Hook name | Arguments | Description +| -------------------- | ------------ | ----------- +| `api-party:request` | `ctx, event` | Called before each request is made. Can be used to log or modify the request. +| `api-party:response` | `ctx, event` | Called after each request is made. Can be used to log or modify the response. + diff --git a/src/module.ts b/src/module.ts index 8d62d4e..c373d24 100644 --- a/src/module.ts +++ b/src/module.ts @@ -5,8 +5,10 @@ import { camelCase, pascalCase } from 'scule' import { createJiti } from 'jiti' import { addImportsSources, addServerHandler, addTemplate, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' import type { HookResult } from '@nuxt/schema' +import type { H3Event } from 'h3' import type { OpenAPI3, OpenAPITSOptions } from 'openapi-typescript' import type { QueryObject } from 'ufo' +import type { FetchContext } from 'ofetch' import { name } from '../package.json' import { generateDeclarationTypes } from './openapi' @@ -92,6 +94,18 @@ declare module '@nuxt/schema' { 'api-party:extend': (options: ModuleOptions) => HookResult } } +declare module '#app' { + interface RuntimeNuxtHooks { + 'api-party:request': (options: FetchContext) => HookResult + 'api-party:response': (options: FetchContext & { response: Response }) => HookResult + } +} +declare module 'nitropack' { + interface NitroRuntimeHooks { + 'api-party:request': (options: FetchContext, event?: H3Event) => HookResult + 'api-party:response': (options: FetchContext & { response: Response }, event?: H3Event) => HookResult + } +} export default defineNuxtModule({ meta: { diff --git a/src/runtime/composables/$api.ts b/src/runtime/composables/$api.ts index 0db8258..3095622 100644 --- a/src/runtime/composables/$api.ts +++ b/src/runtime/composables/$api.ts @@ -9,6 +9,7 @@ import type { ModuleOptions } from '../../module' import { CACHE_KEY_PREFIX } from '../constants' import type { EndpointFetchOptions } from '../types' import type { FetchResponseData, FilterMethods, MethodOption, ParamsOption, RequestBodyOption } from '../openapi' +import { mergeFetchHooks } from '../hooks' import { useNuxtApp, useRequestHeaders, useRuntimeConfig } from '#imports' export interface SharedFetchOptions { @@ -113,8 +114,18 @@ export function _$api( const endpoint = (apiParty.endpoints || {})[endpointId] + const fetchHooks = mergeFetchHooks(fetchOptions, { + onRequest: async (ctx) => { + await nuxt.callHook('api-party:request', ctx) + }, + onResponse: async (ctx) => { + await nuxt.callHook('api-party:response', ctx) + }, + }) + const clientFetcher = () => globalThis.$fetch(resolvePathParams(path, pathParams), { ...fetchOptions, + ...fetchHooks, baseURL: endpoint.url, method, query: { @@ -132,6 +143,7 @@ export function _$api( const serverFetcher = async () => (await globalThis.$fetch(joinURL('/api', apiParty.server.basePath!, endpointId), { ...fetchOptions, + ...fetchHooks, method: 'POST', body: { path: resolvePathParams(path, pathParams), diff --git a/src/runtime/composables/tsconfig.json b/src/runtime/composables/tsconfig.json new file mode 100644 index 0000000..3921d7e --- /dev/null +++ b/src/runtime/composables/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../.nuxt/tsconfig.json" +} diff --git a/src/runtime/composables/useApiData.ts b/src/runtime/composables/useApiData.ts index 8572e94..5d51055 100644 --- a/src/runtime/composables/useApiData.ts +++ b/src/runtime/composables/useApiData.ts @@ -12,6 +12,7 @@ import { headersToObject, serializeMaybeEncodedBody } from '../utils' import { isFormData } from '../form-data' import type { EndpointFetchOptions } from '../types' import type { FetchResponseData, FetchResponseError, FilterMethods, ParamsOption, RequestBodyOption } from '../openapi' +import { mergeFetchHooks } from '../hooks' import { useAsyncData, useRequestHeaders, useRuntimeConfig } from '#imports' type ComputedOptions = { @@ -56,10 +57,6 @@ export type SharedAsyncDataOptions = Omit = Pick< ComputedOptions>, - | 'onRequest' - | 'onRequestError' - | 'onResponse' - | 'onResponseError' | 'query' | 'headers' | 'method' @@ -67,6 +64,12 @@ export type UseApiDataOptions = Pick< | 'retryDelay' | 'retryStatusCodes' | 'timeout' +> & Pick< + NitroFetchOptions, + | 'onRequest' + | 'onRequestError' + | 'onResponse' + | 'onResponseError' > & { path?: MaybeRefOrGetter> body?: MaybeRef | FormData | null> @@ -191,10 +194,20 @@ export function _useApiData( let result: T | undefined + const fetchHooks = mergeFetchHooks(fetchOptions, { + onRequest: async (ctx) => { + await nuxt?.callHook('api-party:request', ctx) + }, + onResponse: async (ctx) => { + await nuxt?.callHook('api-party:response', ctx) + }, + }) + try { if (client) { result = (await globalThis.$fetch(_path.value, { ..._fetchOptions, + ...fetchHooks, signal: controller.signal, baseURL: endpoint.url, method: _endpointFetchOptions.method, @@ -215,6 +228,7 @@ export function _useApiData( joinURL('/api', apiParty.server.basePath!, endpointId), { ..._fetchOptions, + ...fetchHooks, signal: controller.signal, method: 'POST', body: { diff --git a/src/runtime/hooks.ts b/src/runtime/hooks.ts new file mode 100644 index 0000000..d0eec72 --- /dev/null +++ b/src/runtime/hooks.ts @@ -0,0 +1,21 @@ +import type { FetchHooks } from 'ofetch' + +type Arrayify = { [P in keyof T]-?: Extract } +type Hooks = Arrayify> + +export function mergeFetchHooks(...hooks: FetchHooks[]): Hooks { + const result: Hooks = { + onRequest: [], + onResponse: [], + } + + hooks.forEach((hook) => { + for (const name of Object.keys(result) as (keyof Hooks)[]) { + if (hook) { + result[name].push(...(Array.isArray(hook) ? hook : [hook])) + } + } + }) + + return result +} diff --git a/src/runtime/server/$api.ts b/src/runtime/server/$api.ts index 1823e26..581a639 100644 --- a/src/runtime/server/$api.ts +++ b/src/runtime/server/$api.ts @@ -2,7 +2,8 @@ import { headersToObject } from '../utils' import { resolvePathParams } from '../openapi' import type { ModuleOptions } from '../../module' import type { ApiClientFetchOptions } from '../composables/$api' -import { useRuntimeConfig } from '#imports' +import { mergeFetchHooks } from '../hooks' +import { useNitroApp, useRuntimeConfig } from '#imports' export function _$api( endpointId: string, @@ -14,8 +15,18 @@ export function _$api( const endpoints = apiParty.endpoints || {} const endpoint = endpoints[endpointId] + const nitro = useNitroApp() + return globalThis.$fetch(resolvePathParams(path, pathParams), { ...fetchOptions, + ...mergeFetchHooks(fetchOptions, { + onRequest: async (ctx) => { + await nitro.hooks.callHook('api-party:request', ctx) + }, + onResponse: async (ctx) => { + await nitro.hooks.callHook('api-party:response', ctx) + }, + }), baseURL: endpoint.url, query: { ...endpoint.query, diff --git a/src/runtime/server/handler.ts b/src/runtime/server/handler.ts index 3ebd0c6..430a07b 100644 --- a/src/runtime/server/handler.ts +++ b/src/runtime/server/handler.ts @@ -12,9 +12,10 @@ import { import { deserializeMaybeEncodedBody } from '../utils' import type { ModuleOptions } from '../../module' import type { EndpointFetchOptions } from '../types' -import { useRuntimeConfig } from '#imports' +import { useRuntimeConfig, useNitroApp } from '#imports' export default defineEventHandler(async (event) => { + const nitro = useNitroApp() const endpointId = getRouterParam(event, 'endpointId')! const apiParty = useRuntimeConfig().apiParty as Required const endpoints = apiParty.endpoints || {} @@ -79,6 +80,13 @@ export default defineEventHandler(async (event) => { ...(body && { body: await deserializeMaybeEncodedBody(body) }), responseType: 'arrayBuffer', ignoreResponseError: true, + + onRequest: async (ctx) => { + await nitro.hooks.callHook('api-party:request', ctx, event) + }, + onResponse: async (ctx) => { + await nitro.hooks.callHook('api-party:response', ctx, event) + }, }, ) diff --git a/src/runtime/server/tsconfig.json b/src/runtime/server/tsconfig.json new file mode 100644 index 0000000..0e35e64 --- /dev/null +++ b/src/runtime/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../.nuxt/tsconfig.server.json" +}