diff --git a/examples/nuxt3/complete/package.json b/examples/nuxt3/complete/package.json index 7c594f5197..e6bba1385a 100644 --- a/examples/nuxt3/complete/package.json +++ b/examples/nuxt3/complete/package.json @@ -6,9 +6,13 @@ "start": "node .output/server/index.mjs" }, "devDependencies": { - "nuxt3": "^3.0.0-27249044.7d918e1" + "nuxt3": "^3.0.0-27254511.41ff3b8" }, "dependencies": { - "unenv": "^0.4.0" + "@graphql-ez/nuxt": "workspace:^0.1.0", + "@graphql-ez/plugin-codegen": "workspace:^0.7.4", + "@graphql-ez/plugin-schema": "workspace:^0.8.0", + "graphql": "15.4.0-experimental-stream-defer.1", + "graphql-ez": "workspace:^0.13.2" } } diff --git a/examples/nuxt3/complete/server/api/graphql.ts b/examples/nuxt3/complete/server/api/graphql.ts new file mode 100644 index 0000000000..83846a4201 --- /dev/null +++ b/examples/nuxt3/complete/server/api/graphql.ts @@ -0,0 +1,31 @@ +import { CreateApp } from '@graphql-ez/nuxt'; +import { ezCodegen } from '@graphql-ez/plugin-codegen'; +import { ezSchema, gql } from '@graphql-ez/plugin-schema'; + +const { buildApp } = CreateApp({ + ez: { + plugins: [ + ezSchema({ + schema: { + typeDefs: gql` + type Query { + hello: String! + } + `, + resolvers: { + Query: { + hello() { + return 'Hello World!'; + }, + }, + }, + }, + }), + ezCodegen(), + ], + }, +}); + +const { apiHandler } = buildApp(); + +export default apiHandler; diff --git a/examples/nuxt3/complete/src/ez.generated.ts b/examples/nuxt3/complete/src/ez.generated.ts new file mode 100644 index 0000000000..25015b0efe --- /dev/null +++ b/examples/nuxt3/complete/src/ez.generated.ts @@ -0,0 +1,119 @@ +import type { GraphQLResolveInfo } from 'graphql'; +import type { EZContext } from 'graphql-ez'; +export type Maybe = T | null; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise> | import('graphql-ez').DeepPartial; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; +}; + +export type Query = { + __typename?: 'Query'; + hello: Scalars['String']; +}; + +export type ResolverTypeWrapper = Promise | T; + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver = + | ResolverFn + | ResolverWithResolve; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterator | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn = ( + obj: T, + context: TContext, + info: GraphQLResolveInfo +) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Query: ResolverTypeWrapper<{}>; + String: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; + Int: ResolverTypeWrapper; +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Query: {}; + String: Scalars['String']; + Boolean: Scalars['Boolean']; + Int: Scalars['Int']; +}; + +export type QueryResolvers< + ContextType = EZContext, + ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] +> = { + hello?: Resolver; +}; + +export type Resolvers = { + Query?: QueryResolvers; +}; + +declare module 'graphql-ez' { + interface EZResolvers extends Resolvers {} +} diff --git a/package.json b/package.json index 4c4e31f0d1..65577b6f9a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "dev:mjs:yoga:express": "pnpm -r --filter @graphql-yoga/express dev -- --onSuccess \"cross-env PORT=3008 pnpm -r --filter example-yoga-express dev:mjs\"", "dev:next": "pnpm -r --filter @graphql-ez/nextjs dev -- --onSuccess \"pnpm -r --filter example-complete-nextjs dev -- -p 3006\"", "postinstall": "node patch.cjs", - "prepare": "husky install && bob-esbuild tsc && pnpm -r prepack", + "prepare": "husky install && bob-esbuild tsc && (pnpm -r prepack || exit 0)", "pretty": "prettier --write \"**/*.{ts,tsx,json,yaml,js,cjs,mjs,mdx,md}\"", "pretty:check": "prettier --check \"**/*.{ts,tsx,json,yaml,js,cjs,mjs,mdx}\"", "test": "jest", diff --git a/packages/core/main/src/types/app.ts b/packages/core/main/src/types/app.ts index f7e23b4304..e8a8feeb2f 100644 --- a/packages/core/main/src/types/app.ts +++ b/packages/core/main/src/types/app.ts @@ -72,7 +72,8 @@ export type IntegrationsNames = | 'hapi' | 'cloudflare' | 'sveltekit' - | 'vercel'; + | 'vercel' + | 'nuxt'; export interface AdapterFactoryContext { integrationName: IntegrationsNames; diff --git a/packages/nuxt/main/package.json b/packages/nuxt/main/package.json new file mode 100644 index 0000000000..a83b541b46 --- /dev/null +++ b/packages/nuxt/main/package.json @@ -0,0 +1,64 @@ +{ + "name": "@graphql-ez/nuxt", + "version": "0.1.0", + "homepage": "https://www.graphql-ez.com", + "repository": { + "type": "git", + "url": "https://github.com/PabloSzx/graphql-ez", + "directory": "packages/nuxt" + }, + "license": "MIT", + "author": "PabloSz ", + "sideEffects": false, + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./*": { + "require": "./dist/*.js", + "import": "./dist/*.mjs" + } + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "scripts": { + "dev": "bob-esbuild watch", + "prepack": "bob-esbuild build", + "postpublish": "gh-release" + }, + "dependencies": { + "@graphql-ez/utils": "workspace:^0.1.2", + "h3": "^0.3.3" + }, + "devDependencies": { + "@types/node": "^16.11.3", + "changesets-github-release": "^0.0.4", + "graphql": "15.4.0-experimental-stream-defer.1", + "graphql-ez": "workspace:^0.13.2", + "jest": "^27.3.1", + "nuxt3": "^3.0.0-27254511.41ff3b8", + "typescript": "^4.4.4" + }, + "peerDependencies": { + "@types/node": "*", + "graphql": "*", + "graphql-ez": "workspace:^0.13.2", + "nuxt3": "*" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + }, + "nuxt3": { + "optional": true + } + }, + "engines": { + "node": "^12.20.0 || >=14.13.0" + }, + "publishConfig": { + "directory": "dist" + } +} diff --git a/packages/nuxt/main/src/index.ts b/packages/nuxt/main/src/index.ts new file mode 100644 index 0000000000..0bca9816e6 --- /dev/null +++ b/packages/nuxt/main/src/index.ts @@ -0,0 +1,169 @@ +import { + AppOptions, + BaseAppBuilder, + BuildAppOptions, + createEZAppFactory, + EZAppFactoryType, + GetEnvelopedFn, + handleRequest, + InternalAppBuildContext, + InternalAppBuildContextKey, + InternalAppBuildIntegrationContext, + LazyPromise, +} from 'graphql-ez'; +import { useBody, useQuery } from 'h3'; +import type { IncomingMessage, ServerResponse } from 'http'; + +export interface NuxtAppOptions extends AppOptions {} + +export interface EZAppBuilder extends BaseAppBuilder { + readonly buildApp: (options?: BuildAppOptions) => EZApp; +} + +export type NuxtApiHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise; + +export interface EZApp { + readonly getEnveloped: Promise>; + readonly apiHandler: NuxtApiHandler; + + readonly ready: Promise; + + [InternalAppBuildContextKey]: InternalAppBuildContext; +} + +export * from 'graphql-ez'; + +declare module 'graphql-ez' { + interface BuildContextArgs { + nuxt?: { + req: IncomingMessage; + res: ServerResponse; + }; + } + + interface InternalAppBuildIntegrationContext { + nuxt?: NuxtHandlerContext; + } +} + +export interface NuxtHandlerContext {} + +export function CreateApp(config: NuxtAppOptions = {}): EZAppBuilder { + const appConfig = { ...config }; + + let ezApp: EZAppFactoryType; + + try { + ezApp = createEZAppFactory( + { + integrationName: 'nuxt', + }, + appConfig + ); + } catch (err) { + err instanceof Error && Error.captureStackTrace(err, CreateApp); + + throw err; + } + + const { appBuilder, onIntegrationRegister, ...commonApp } = ezApp; + + const buildApp: EZAppBuilder['buildApp'] = function buildApp(buildOptions = {}) { + const { buildContext, onAppRegister } = appConfig; + + let appHandler: NuxtApiHandler; + + const appPromise = Promise.allSettled([ + appBuilder(buildOptions, async ({ ctx, getEnveloped }) => { + const integration: InternalAppBuildIntegrationContext = { + nuxt: {}, + }; + + if (onAppRegister) await onAppRegister({ ctx, integration, getEnveloped }); + + await onIntegrationRegister(integration); + + const { + preProcessRequest, + options: { customHandleRequest }, + } = ctx; + + const requestHandler = customHandleRequest || handleRequest; + + const EZHandler: NuxtApiHandler = async function EZHandler(req, res) { + const request = { + body: await useBody(req), + headers: req.headers, + method: req.method || 'POST', + query: useQuery(req), + }; + + return requestHandler({ + req, + request, + getEnveloped, + baseOptions: appConfig, + contextArgs() { + return { + req, + nuxt: { + req, + res, + }, + }; + }, + buildContext, + onResponse(result, defaultHandle) { + return defaultHandle(req, res, result); + }, + onMultiPartResponse(result, defaultHandle) { + return defaultHandle(req, res, result); + }, + onPushResponse(result, defaultHandle) { + return defaultHandle(req, res, result); + }, + processRequestOptions: undefined, + preProcessRequest, + }); + }; + + return (appHandler = EZHandler); + }), + ]).then(v => v[0]); + + return { + apiHandler: async function handler(req, res) { + if (appHandler) { + await appHandler(req, res); + } else { + const result = await appPromise; + if (result.status === 'rejected') + throw Error( + process.env.NODE_ENV === 'development' + ? 'Error while building EZ App: ' + (result.reason?.message || JSON.stringify(result.reason)) + : 'Unexpected Error' + ); + + await ( + await result.value.app + )(req, res); + } + }, + getEnveloped: LazyPromise(async () => { + const result = await appPromise; + if (result.status === 'rejected') throw result.reason; + return result.value.getEnveloped; + }), + ready: LazyPromise(async () => { + const result = await appPromise; + if (result.status === 'rejected') throw result.reason; + }), + [InternalAppBuildContextKey]: commonApp[InternalAppBuildContextKey], + }; + }; + + return { + ...commonApp, + buildApp, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2996b77abb..d98a857f6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -716,10 +716,18 @@ importers: examples/nuxt3/complete: specifiers: - nuxt3: ^3.0.0-27249044.7d918e1 - unenv: ^0.4.0 + '@graphql-ez/nuxt': workspace:^0.1.0 + '@graphql-ez/plugin-codegen': workspace:^0.7.4 + '@graphql-ez/plugin-schema': workspace:^0.8.0 + graphql: 15.4.0-experimental-stream-defer.1 + graphql-ez: workspace:^0.13.2 + nuxt3: ^3.0.0-27254511.41ff3b8 dependencies: - unenv: 0.4.0 + '@graphql-ez/nuxt': link:../../../packages/nuxt/main + '@graphql-ez/plugin-codegen': link:../../../packages/plugin/codegen + '@graphql-ez/plugin-schema': link:../../../packages/plugin/schema + graphql: 15.4.0-experimental-stream-defer.1 + graphql-ez: link:../../../packages/core/main devDependencies: nuxt3: 3.0.0-27254511.41ff3b8_esbuild@0.13.9 @@ -1485,6 +1493,29 @@ importers: graphql-ez: link:../../core/main next: 12.0.0 + packages/nuxt/main: + specifiers: + '@graphql-ez/utils': workspace:^0.1.2 + '@types/node': '>=16.11.1' + changesets-github-release: ^0.0.4 + graphql: 15.4.0-experimental-stream-defer.1 + graphql-ez: workspace:^0.13.2 + h3: ^0.3.3 + jest: ^27.3.1 + nuxt3: ^3.0.0-27254511.41ff3b8 + typescript: '>=4.4.4' + dependencies: + '@graphql-ez/utils': link:../../core/utils + h3: 0.3.3 + devDependencies: + '@types/node': 16.11.6 + changesets-github-release: 0.0.4 + graphql: 15.4.0-experimental-stream-defer.1 + graphql-ez: link:../../core/main + jest: 27.3.1 + nuxt3: 3.0.0-27254511.41ff3b8_esbuild@0.13.9 + typescript: 4.4.4 + packages/plugin/altair: specifiers: '@graphql-ez/utils': workspace:^0.1.2 @@ -7765,6 +7796,7 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: true /builtin-modules/3.2.0: resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==} @@ -9062,6 +9094,7 @@ packages: /defu/5.0.0: resolution: {integrity: sha512-VHg73EDeRXlu7oYWRmmrNp/nl7QkdXUxkQQKig0Zk8daNmm84AbGoC8Be6/VVLJEKxn12hR0UBmz8O+xQiAPKQ==} + dev: true /del/5.1.0: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} @@ -10950,7 +10983,6 @@ packages: /h3/0.3.3: resolution: {integrity: sha512-ammvVddtZArv6pnCkl0tEekY8owWPZNZCW4teePYzGwfN2w7kb0wnraLIFnB20mqUU2kCAV5bvI+2mjmGztS3w==} - dev: true /hard-rejection/2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} @@ -13416,6 +13448,7 @@ packages: resolution: {integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==} engines: {node: '>=4.0.0'} hasBin: true + dev: true /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -18183,6 +18216,7 @@ packages: process: 0.11.10 upath: 2.0.1 util: 0.12.4 + dev: true /unfetch/4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -18412,6 +18446,7 @@ packages: /upath/2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} + dev: true /upper-case-first/2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}