Skip to content

Commit

Permalink
feat: $myApi composables in Nitro routes
Browse files Browse the repository at this point in the history
  • Loading branch information
johannschopplich committed Aug 19, 2023
1 parent ed12751 commit cd8481f
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 73 deletions.
8 changes: 4 additions & 4 deletions docs/api/my-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ interface BaseApiFetchOptions {
cache?: boolean
}

type AnyApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
type ApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
pathParams?: Record<string, string>
body?: string | Record<string, any> | FormData | null
} & BaseApiFetchOptions
}

type AnyApi = <T = any>(
type $Api = <T = any>(
path: string,
opts?: AnyApiFetchOptions,
opts?: ApiFetchOptions & BaseApiFetchOptions
) => Promise<T>
```
8 changes: 4 additions & 4 deletions docs/api/use-my-api-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const { data, pending, refresh, error } = await useJsonPlaceholderData('comments
// Custom headers to be sent with the request
headers: {
'X-Foo': 'bar'
},
}
})
</script>
Expand Down Expand Up @@ -147,7 +147,7 @@ type BaseUseApiDataOptions<T> = Omit<AsyncDataOptions<T>, 'watch'> & {
watch?: (WatchSource<unknown> | object)[] | false
}

type UseAnyApiDataOptions<T> = Pick<
type UseApiDataOptions<T> = Pick<
ComputedOptions<NitroFetchOptions<string>>,
| 'onRequest'
| 'onRequestError'
Expand All @@ -162,8 +162,8 @@ type UseAnyApiDataOptions<T> = Pick<
body?: MaybeRef<string | Record<string, any> | FormData | null | undefined>
} & BaseUseApiDataOptions<T>

type UseAnyApiData = <T = any>(
type UseApiData = <T = any>(
path: MaybeRefOrGetter<string>,
opts?: UseAnyApiDataOptions<T>,
opts?: UseApiDataOptions<T>
) => AsyncData<T, FetchError>
```
4 changes: 2 additions & 2 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default defineNuxtConfig({
+ token: '<your-api-token>',
+ query: {},
+ headers: {},
+ },
},
+ }
}
}
})
```
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/openapi-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
})
```

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/retries.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Example:
```ts
// Retry failed requests 3 times
const { data } = await useJsonPlaceholderData('posts/1', {
retry: 3,
retry: 3
})
```

Expand Down
4 changes: 2 additions & 2 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</NuxtLink>
<p>
Requests are proxied by a Nuxt server route and passed back to the client.
The playground uses <a href="https://jsonplaceholder.typicode.com/">{JSON} Placeholder</a>
and <a href="https://petstore3.swagger.io/">Swagger Petstore</a> as example APIs.
The playground uses <a href="https://jsonplaceholder.typicode.com/">JSON Placeholder</a>
and <a href="https://petstore3.swagger.io/">Swagger Pet Store</a> as example APIs.
The dynamic composables <code>$jsonPlaceholder</code> and <code>useJsonPlaceholderData</code>
are generated by the module.
</p>
Expand Down
11 changes: 10 additions & 1 deletion playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ul>
<li>
<NuxtLink to="/jsonPlaceholder">
{JSON} Placeholder
JSON Placeholder
</NuxtLink>
</li>
<li>
Expand All @@ -12,4 +12,13 @@
</NuxtLink>
</li>
</ul>

<h2>Server Routes (Nitro)</h2>
<ul>
<li>
<NuxtLink to="/api/_jsonPlaceholder" target="_blank">
JSON Placeholder (Server)
</NuxtLink>
</li>
</ul>
</template>
6 changes: 6 additions & 0 deletions playground/server/api/_jsonPlaceholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// import { $jsonPlaceholder } from '#nuxt-api-party/server'

export default defineEventHandler(async () => {
const response = await $jsonPlaceholder('posts/1')
return response
})
96 changes: 66 additions & 30 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,28 +122,70 @@ export default defineNuxtModule<ModuleOptions>({
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)]),
Expand All @@ -152,11 +194,6 @@ export default defineNuxtModule<ModuleOptions>({
// 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`,
Expand All @@ -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({
Expand All @@ -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`) })
})
}
},
Expand Down
15 changes: 5 additions & 10 deletions src/runtime/composables/$api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export interface BaseApiFetchOptions {
cache?: boolean
}

export type AnyApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
export type ApiFetchOptions = Omit<NitroFetchOptions<string>, 'body' | 'cache'> & {
pathParams?: Record<string, string>
body?: string | Record<string, any> | FormData | null
} & BaseApiFetchOptions
}

export type $AnyApi = <T = any>(
export type $Api = <T = any>(
path: string,
opts?: AnyApiFetchOptions,
opts?: ApiFetchOptions & BaseApiFetchOptions,
) => Promise<T>

export interface $OpenApi<Paths extends Record<string, PathItemObject>> {
Expand All @@ -41,15 +41,10 @@ export interface $OpenApi<Paths extends Record<string, PathItemObject>> {
): Promise<OpenApiResponse<Paths[`/${P}`][Lowercase<M>]>>
}

/** @remarks Prefer using `$AnyApi` and `$OpenApi` directly */
export type $Api<Paths extends Record<string, PathItemObject> = never> = [Paths] extends [never]
? $AnyApi
: $OpenApi<Paths>

export function _$api<T = any>(
endpointId: string,
path: string,
opts: AnyApiFetchOptions = {},
opts: ApiFetchOptions & BaseApiFetchOptions = {},
): Promise<T> {
const nuxt = useNuxtApp()
const promiseMap = (nuxt._promiseMap = nuxt._promiseMap || new Map()) as Map<string, Promise<T>>
Expand Down
13 changes: 4 additions & 9 deletions src/runtime/composables/useApiData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type BaseUseApiDataOptions<T> = Omit<AsyncDataOptions<T>, 'watch'> & {
watch?: (WatchSource<unknown> | object)[] | false
}

export type UseAnyApiDataOptions<T> = Pick<
export type UseApiDataOptions<T> = Pick<
ComputedOptions<NitroFetchOptions<string>>,
| 'onRequest'
| 'onRequestError'
Expand All @@ -58,9 +58,9 @@ export type UseOpenApiDataOptions<
M extends IgnoreCase<keyof P & HttpMethod>,
> = BaseUseApiDataOptions<OpenApiResponse<P[Lowercase<M>]>> & ComputedOptions<OpenApiRequestOptions<P, M>>

export type UseAnyApiData = <T = any>(
export type UseApiData = <T = any>(
path: MaybeRefOrGetter<string>,
opts?: UseAnyApiDataOptions<T>,
opts?: UseApiDataOptions<T>,
) => AsyncData<T, FetchError>

export interface UseOpenApiData<Paths extends Record<string, PathItemObject>> {
Expand All @@ -73,15 +73,10 @@ export interface UseOpenApiData<Paths extends Record<string, PathItemObject>> {
): AsyncData<OpenApiResponse<Paths[`/${P}`][Lowercase<M>]>, OpenApiError<Paths[`/${P}`][Lowercase<M>]>>
}

/** @remarks Prefer using `UseAnyApiData` and `UseOpenApiData` directly */
export type UseApiData<Paths extends Record<string, PathItemObject> = never> = [Paths] extends [never]
? UseAnyApiData
: UseOpenApiData<Paths>

export function _useApiData<T = any>(
endpointId: string,
path: MaybeRefOrGetter<string>,
opts: UseAnyApiDataOptions<T> = {},
opts: UseApiDataOptions<T> = {},
) {
const { apiParty } = useRuntimeConfig().public
const {
Expand Down
31 changes: 31 additions & 0 deletions src/runtime/server/$api.ts
Original file line number Diff line number Diff line change
@@ -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<T = any>(
endpointId: string,
path: string,
opts: ApiFetchOptions = {},
): Promise<T> {
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<T>(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<T>
}
Loading

0 comments on commit cd8481f

Please sign in to comment.