From cd8481f6d8669d51abeeebc9e122b7f0848e9200 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Sat, 19 Aug 2023 22:34:48 +0200 Subject: [PATCH] feat: $myApi composables in Nitro routes --- docs/api/my-api.md | 8 +- docs/api/use-my-api-data.md | 8 +- docs/guide/migration.md | 4 +- docs/guide/openapi-types.md | 6 +- docs/guide/retries.md | 2 +- playground/app.vue | 4 +- playground/pages/index.vue | 11 ++- playground/server/api/_jsonPlaceholder.ts | 6 ++ src/module.ts | 96 ++++++++++++++------ src/runtime/composables/$api.ts | 15 +-- src/runtime/composables/useApiData.ts | 13 +-- src/runtime/server/$api.ts | 31 +++++++ src/runtime/{server.ts => server/handler.ts} | 8 +- src/runtime/utils.ts | 6 +- 14 files changed, 145 insertions(+), 73 deletions(-) create mode 100644 playground/server/api/_jsonPlaceholder.ts create mode 100644 src/runtime/server/$api.ts rename src/runtime/{server.ts => server/handler.ts} (92%) diff --git a/docs/api/my-api.md b/docs/api/my-api.md index 70af6d8..d69a633 100644 --- a/docs/api/my-api.md +++ b/docs/api/my-api.md @@ -108,13 +108,13 @@ interface BaseApiFetchOptions { cache?: boolean } -type AnyApiFetchOptions = Omit, 'body' | 'cache'> & { +type ApiFetchOptions = Omit, 'body' | 'cache'> & { pathParams?: Record body?: string | Record | FormData | null -} & BaseApiFetchOptions +} -type AnyApi = ( +type $Api = ( path: string, - opts?: AnyApiFetchOptions, + opts?: ApiFetchOptions & BaseApiFetchOptions ) => Promise ``` diff --git a/docs/api/use-my-api-data.md b/docs/api/use-my-api-data.md index 739729f..7c8c1a1 100644 --- a/docs/api/use-my-api-data.md +++ b/docs/api/use-my-api-data.md @@ -80,7 +80,7 @@ const { data, pending, refresh, error } = await useJsonPlaceholderData('comments // Custom headers to be sent with the request headers: { 'X-Foo': 'bar' - }, + } }) @@ -147,7 +147,7 @@ type BaseUseApiDataOptions = Omit, 'watch'> & { watch?: (WatchSource | object)[] | false } -type UseAnyApiDataOptions = Pick< +type UseApiDataOptions = Pick< ComputedOptions>, | 'onRequest' | 'onRequestError' @@ -162,8 +162,8 @@ type UseAnyApiDataOptions = Pick< body?: MaybeRef | FormData | null | undefined> } & BaseUseApiDataOptions -type UseAnyApiData = ( +type UseApiData = ( path: MaybeRefOrGetter, - opts?: UseAnyApiDataOptions, + opts?: UseApiDataOptions ) => AsyncData ``` diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 37d5eb7..4727453 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -22,8 +22,8 @@ export default defineNuxtConfig({ + token: '', + query: {}, + headers: {}, -+ }, - }, ++ } + } } }) ``` diff --git a/docs/guide/openapi-types.md b/docs/guide/openapi-types.md index 9fd20e8..7472961 100644 --- a/docs/guide/openapi-types.md +++ b/docs/guide/openapi-types.md @@ -106,9 +106,9 @@ export default defineNuxtConfig({ apiParty: { myApi: { url: process.env.MY_API_API_BASE_URL!, - schema: './schemas/myApi.yaml', - }, - }, + schema: './schemas/myApi.yaml' + } + } }) ``` diff --git a/docs/guide/retries.md b/docs/guide/retries.md index 338d9bd..2dae34b 100644 --- a/docs/guide/retries.md +++ b/docs/guide/retries.md @@ -13,7 +13,7 @@ Example: ```ts // Retry failed requests 3 times const { data } = await useJsonPlaceholderData('posts/1', { - retry: 3, + retry: 3 }) ``` diff --git a/playground/app.vue b/playground/app.vue index 25eb9d5..4911ec4 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -10,8 +10,8 @@

Requests are proxied by a Nuxt server route and passed back to the client. - The playground uses {JSON} Placeholder - and Swagger Petstore as example APIs. + The playground uses JSON Placeholder + and Swagger Pet Store as example APIs. The dynamic composables $jsonPlaceholder and useJsonPlaceholderData are generated by the module.

diff --git a/playground/pages/index.vue b/playground/pages/index.vue index cc73726..84f3d54 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -3,7 +3,7 @@
  • - {JSON} Placeholder + JSON Placeholder
  • @@ -12,4 +12,13 @@
+ +

Server Routes (Nitro)

+
    +
  • + + JSON Placeholder (Server) + +
  • +
diff --git a/playground/server/api/_jsonPlaceholder.ts b/playground/server/api/_jsonPlaceholder.ts new file mode 100644 index 0000000..509370d --- /dev/null +++ b/playground/server/api/_jsonPlaceholder.ts @@ -0,0 +1,6 @@ +// import { $jsonPlaceholder } from '#nuxt-api-party/server' + +export default defineEventHandler(async () => { + const response = await $jsonPlaceholder('posts/1') + return response +}) diff --git a/src/module.ts b/src/module.ts index b391c83..d653b78 100644 --- a/src/module.ts +++ b/src/module.ts @@ -122,28 +122,70 @@ export default defineNuxtModule({ const { resolve } = createResolver(import.meta.url) nuxt.options.build.transpile.push(resolve('runtime')) + const relativeTo = (path: string) => relative( + resolve(nuxt.options.rootDir, nuxt.options.buildDir, 'module'), + resolve(path), + ) + + const endpointKeys = Object.keys(resolvedOptions.endpoints) + const schemaEndpoints = Object.fromEntries( + Object.entries(resolvedOptions.endpoints) + .filter(([, endpoint]) => 'schema' in endpoint), + ) + const schemaEndpointIds = Object.keys(schemaEndpoints) + const hasOpenAPIPkg = await tryResolveModule('openapi-typescript', [nuxt.options.rootDir]) + + if (schemaEndpointIds.length && !hasOpenAPIPkg) { + logger.warn('OpenAPI types generation is enabled, but the `openapi-typescript` package is not found. Please install it to enable endpoint types generation.') + schemaEndpointIds.length = 0 + } + // Add Nuxt server route to proxy the API request server-side addServerHandler({ route: '/api/__api_party/:endpointId', method: 'post', - handler: resolve('runtime/server'), + handler: resolve('runtime/server/handler'), }) nuxt.hook('nitro:config', (config) => { - // Inline server handler into Nitro bundle + // Inline local server handler dependencies into Nitro bundle // Needed to circumvent "cannot find module" error in `server.ts` for the `utils` import config.externals ||= {} config.externals.inline ||= [] config.externals.inline.push(...[ resolve('runtime/utils'), resolve('runtime/formData'), + resolve('runtime/server/$api'), ]) - }) - const endpointKeys = Object.keys(resolvedOptions.endpoints) + // Provide `#nuxt-api-party/server` module alias for Nitro + config.alias ||= {} + config.alias[`#${moduleName}/server`] = resolve(nuxt.options.buildDir, `module/${moduleName}-nitro`) + + config.virtual ||= {} + config.virtual[`#${moduleName}/server`] = () => ` +import { _$api } from '${resolve('runtime/server/$api')}' +${endpointKeys.map(i => ` +export const ${getRawComposableName(i)} = (...args) => _$api('${i}', ...args) +`.trimStart()).join('')}`.trimStart() + + if (schemaEndpointIds.length) { + config.typescript ||= {} + config.typescript.tsConfig ||= {} + config.typescript.tsConfig.include ||= [] + config.typescript.tsConfig.include.push(`./module/${moduleName}-schema.d.ts`) + } + + // Add Nitro auto-imports for generated composables + config.imports = defu(config.imports, { + presets: endpointKeys.map(i => ({ + from: `#${moduleName}/server`, + imports: [{ name: getRawComposableName(i) }], + })), + }) + }) - // Nuxt will resolve the imports relative to the `srcDir`, so we can't use - // `#nuxt-api-party` with `declare module` pattern here + // Add Nuxt auto-imports for generated composables addImportsSources({ from: resolve(nuxt.options.buildDir, `module/${moduleName}.mjs`), imports: endpointKeys.flatMap(i => [getRawComposableName(i), getDataComposableName(i)]), @@ -152,11 +194,6 @@ export default defineNuxtModule({ // Add `#nuxt-api-party` module alias for generated composables nuxt.options.alias[`#${moduleName}`] = resolve(nuxt.options.buildDir, `module/${moduleName}.mjs`) - const relativeTo = (path: string) => relative( - resolve(nuxt.options.rootDir, nuxt.options.buildDir, 'module'), - resolve(path), - ) - // Add module template for generated composables addTemplate({ filename: `module/${moduleName}.mjs`, @@ -171,39 +208,38 @@ export const ${getDataComposableName(i)} = (...args) => _useApiData('${i}', ...a }, }) - const schemaEndpoints = Object.fromEntries( - Object.entries(resolvedOptions.endpoints) - .filter(([, endpoint]) => 'schema' in endpoint), - ) - const schemaEndpointIds = Object.keys(schemaEndpoints) - const hasOpenAPIPkg = await tryResolveModule('openapi-typescript', [nuxt.options.rootDir]) - - if (schemaEndpointIds.length && !hasOpenAPIPkg) { - logger.warn('OpenAPI types generation is enabled, but the `openapi-typescript` package is not found. Please install it to enable endpoint types generation.') - schemaEndpointIds.length = 0 - } - // Add types for Nuxt auto-imports and the `#nuxt-api-party` module alias addTemplate({ filename: `module/${moduleName}.d.ts`, getContents() { return ` // Generated by ${moduleName} -import type { $Api, $AnyApi, $OpenApi, AnyApiFetchOptions } from '${relativeTo('runtime/composables/$api')}' -import type { UseApiData, UseAnyApiData, UseOpenApiData, UseAnyApiDataOptions } from '${relativeTo('runtime/composables/useApiData')}' +import type { $Api, $OpenApi, ApiFetchOptions } from '${relativeTo('runtime/composables/$api')}' +import type { UseApiData, UseOpenApiData, UseApiDataOptions } from '${relativeTo('runtime/composables/useApiData')}' ${schemaEndpointIds.map(i => `import type { paths as ${pascalCase(i)}Paths } from '#${moduleName}/${i}'`).join('')} -export type { $Api, $AnyApi, $OpenApi, AnyApiFetchOptions, UseApiData, UseAnyApiData, UseOpenApiData, UseAnyApiDataOptions } +export type { $Api, $OpenApi, ApiFetchOptions, UseApiData, UseOpenApiData, UseApiDataOptions } ${endpointKeys.map(i => ` -export declare const ${getRawComposableName(i)}: ${schemaEndpointIds.includes(i) ? `$OpenApi<${pascalCase(i)}Paths>` : '$AnyApi'} -export declare const ${getDataComposableName(i)}: ${schemaEndpointIds.includes(i) ? `UseOpenApiData<${pascalCase(i)}Paths>` : 'UseAnyApiData'} +export declare const ${getRawComposableName(i)}: ${schemaEndpointIds.includes(i) ? `$OpenApi<${pascalCase(i)}Paths>` : '$Api'} +export declare const ${getDataComposableName(i)}: ${schemaEndpointIds.includes(i) ? `UseOpenApiData<${pascalCase(i)}Paths>` : 'UseApiData'} `.trimStart()).join('').trimEnd()} `.trimStart() }, }) + // Add types for Nitro auto-imports and the `#nuxt-api-party/server` module alias + addTemplate({ + filename: `module/${moduleName}-nitro.d.ts`, + async getContents() { + return ` +// Generated by ${moduleName} +export { ${endpointKeys.map(i => getRawComposableName(i)).join(', ')} } from './${moduleName}' +`.trimStart() + }, + }) + // Add type references for endpoints with OpenAPI schemas if (schemaEndpointIds.length) { addTemplate({ @@ -213,8 +249,8 @@ export declare const ${getDataComposableName(i)}: ${schemaEndpointIds.includes(i }, }) - nuxt.hook('prepare:types', (options) => { - options.references.push({ path: resolve(nuxt.options.buildDir, `module/${moduleName}-schema.d.ts`) }) + nuxt.hook('prepare:types', ({ references }) => { + references.push({ path: resolve(nuxt.options.buildDir, `module/${moduleName}-schema.d.ts`) }) }) } }, diff --git a/src/runtime/composables/$api.ts b/src/runtime/composables/$api.ts index 048a21c..ef5627b 100644 --- a/src/runtime/composables/$api.ts +++ b/src/runtime/composables/$api.ts @@ -21,14 +21,14 @@ export interface BaseApiFetchOptions { cache?: boolean } -export type AnyApiFetchOptions = Omit, 'body' | 'cache'> & { +export type ApiFetchOptions = Omit, 'body' | 'cache'> & { pathParams?: Record body?: string | Record | FormData | null -} & BaseApiFetchOptions +} -export type $AnyApi = ( +export type $Api = ( path: string, - opts?: AnyApiFetchOptions, + opts?: ApiFetchOptions & BaseApiFetchOptions, ) => Promise export interface $OpenApi> { @@ -41,15 +41,10 @@ export interface $OpenApi> { ): Promise]>> } -/** @remarks Prefer using `$AnyApi` and `$OpenApi` directly */ -export type $Api = never> = [Paths] extends [never] - ? $AnyApi - : $OpenApi - export function _$api( endpointId: string, path: string, - opts: AnyApiFetchOptions = {}, + opts: ApiFetchOptions & BaseApiFetchOptions = {}, ): Promise { const nuxt = useNuxtApp() const promiseMap = (nuxt._promiseMap = nuxt._promiseMap || new Map()) as Map> diff --git a/src/runtime/composables/useApiData.ts b/src/runtime/composables/useApiData.ts index 0840f77..c30a6b0 100644 --- a/src/runtime/composables/useApiData.ts +++ b/src/runtime/composables/useApiData.ts @@ -38,7 +38,7 @@ export type BaseUseApiDataOptions = Omit, 'watch'> & { watch?: (WatchSource | object)[] | false } -export type UseAnyApiDataOptions = Pick< +export type UseApiDataOptions = Pick< ComputedOptions>, | 'onRequest' | 'onRequestError' @@ -58,9 +58,9 @@ export type UseOpenApiDataOptions< M extends IgnoreCase, > = BaseUseApiDataOptions]>> & ComputedOptions> -export type UseAnyApiData = ( +export type UseApiData = ( path: MaybeRefOrGetter, - opts?: UseAnyApiDataOptions, + opts?: UseApiDataOptions, ) => AsyncData export interface UseOpenApiData> { @@ -73,15 +73,10 @@ export interface UseOpenApiData> { ): AsyncData]>, OpenApiError]>> } -/** @remarks Prefer using `UseAnyApiData` and `UseOpenApiData` directly */ -export type UseApiData = never> = [Paths] extends [never] - ? UseAnyApiData - : UseOpenApiData - export function _useApiData( endpointId: string, path: MaybeRefOrGetter, - opts: UseAnyApiDataOptions = {}, + opts: UseApiDataOptions = {}, ) { const { apiParty } = useRuntimeConfig().public const { diff --git a/src/runtime/server/$api.ts b/src/runtime/server/$api.ts new file mode 100644 index 0000000..455d4cd --- /dev/null +++ b/src/runtime/server/$api.ts @@ -0,0 +1,31 @@ +import { headersToObject, resolvePath } from '../utils' +import type { ModuleOptions } from '../../module' +import type { ApiFetchOptions } from '../composables/$api' +import { useRuntimeConfig } from '#imports' + +export function _$api( + endpointId: string, + path: string, + opts: ApiFetchOptions = {}, +): Promise { + const { pathParams, query, headers, method, body, ...fetchOptions } = opts + const { apiParty } = useRuntimeConfig() + const endpoints = (apiParty as unknown as ModuleOptions).endpoints || {} + const endpoint = endpoints[endpointId] + + return globalThis.$fetch(resolvePath(path, pathParams), { + ...fetchOptions, + baseURL: endpoint.url, + method, + query: { + ...endpoint.query, + ...query, + }, + headers: { + ...(endpoint.token && { Authorization: `Bearer ${endpoint.token}` }), + ...endpoint.headers, + ...headersToObject(headers), + }, + body, + }) as Promise +} diff --git a/src/runtime/server.ts b/src/runtime/server/handler.ts similarity index 92% rename from src/runtime/server.ts rename to src/runtime/server/handler.ts index 63266b0..bf60eed 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server/handler.ts @@ -1,14 +1,14 @@ import { createError, defineEventHandler, getRequestHeader, getRouterParam, readBody } from 'h3' import type { FetchError } from 'ofetch' -import type { ModuleOptions } from '../module' -import { deserializeMaybeEncodedBody } from './utils' -import type { EndpointFetchOptions } from './utils' +import type { ModuleOptions } from '../../module' +import { deserializeMaybeEncodedBody } from '../utils' +import type { EndpointFetchOptions } from '../utils' import { useRuntimeConfig } from '#imports' export default defineEventHandler(async (event): Promise => { const endpointId = getRouterParam(event, 'endpointId')! const { apiParty } = useRuntimeConfig() - const endpoints = (apiParty as unknown as ModuleOptions).endpoints! + const endpoints = (apiParty as unknown as ModuleOptions).endpoints || {} const endpoint = endpoints[endpointId] if (!endpoint) { diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts index a23c969..8a79114 100644 --- a/src/runtime/utils.ts +++ b/src/runtime/utils.ts @@ -1,7 +1,7 @@ import { unref } from 'vue' import type { NitroFetchOptions } from 'nitropack' import type { Ref } from 'vue' -import type { AnyApiFetchOptions } from './composables/$api' +import type { ApiFetchOptions } from './composables/$api' import { formDataToObject, isFormData, isSerializedFormData, objectToFormData } from './formData' export type EndpointFetchOptions = NitroFetchOptions & { @@ -27,14 +27,14 @@ export function headersToObject(headers: HeadersInit = {}): Record