diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a5a31d5..f6f04c7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,12 +7,6 @@ on: paths: - "docs/**" - ".github/workflows/docs.yml" - pull_request: - branches: - - main - paths: - - "docs/**" - - ".github/workflows/docs.yml" workflow_dispatch: jobs: diff --git a/README.md b/README.md index 9144ca8..7646731 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ - 🚀 [Nuxt 3](https://v3.nuxtjs.org) Support - Full Typescript Support - HMR (Hot Module Reload) for GraphQL documents -- Minimal [GraphQL Client](https://github.com/prisma-labs/graphql-request#graphql-request) + [Code Generation](https://www.graphql-code-generator.com/) +- Minimal GraphQL Client + [Code Generation](https://www.graphql-code-generator.com/) ## Preview diff --git a/docs/content/1.getting-started/3.composables.md b/docs/content/1.getting-started/3.composables.md index c38e844..cbb2a7b 100644 --- a/docs/content/1.getting-started/3.composables.md +++ b/docs/content/1.getting-started/3.composables.md @@ -114,9 +114,8 @@ export default defineNuxtPlugin(() => { for (const gqlError of err.gqlErrors) { console.error('[nuxt-graphql-client] [GraphQL error]', { client: err.client, + operation: err.operation, statusCode: err.statusCode, - operationType: err.operationType, - operationName: err.operationName, gqlError }) } diff --git a/docs/content/1.index.md b/docs/content/1.index.md index d941f83..c9b28c9 100755 --- a/docs/content/1.index.md +++ b/docs/content/1.index.md @@ -19,7 +19,7 @@ snippet: yarn add nuxt-graphql-client Nuxt [GraphQL Client]{ .text-primary } #description -Minimal [GraphQL Client](https://github.com/prisma-labs/graphql-request#graphql-request) + [Code Generation](https://www.graphql-code-generator.com/) for [Nuxt](https://v3.nuxtjs.org) ⚡️ +Minimal GraphQL Client + [Code Generation](https://www.graphql-code-generator.com/) for [Nuxt](https://v3.nuxtjs.org) ⚡️ #extra ::list diff --git a/docs/content/4.community/1.credits.md b/docs/content/4.community/1.credits.md index 99ff1d0..f890e55 100644 --- a/docs/content/4.community/1.credits.md +++ b/docs/content/4.community/1.credits.md @@ -10,5 +10,5 @@ Special thanks to [@danielroe](https://github.com/danielroe) for helping navigat Under the hood: -- [GraphQL Request](https://github.com/prisma-labs/graphql-request#graphql-request) +- [ofetch](https://github.com/unjs/ofetch) - [GraphQL Code Generator](https://www.graphql-code-generator.com/) diff --git a/examples/subscription/app.vue b/examples/subscription/app.vue new file mode 100644 index 0000000..352ca55 --- /dev/null +++ b/examples/subscription/app.vue @@ -0,0 +1,64 @@ + + + + + + subscription + + + + + + + WS Status: {{ wsStatus }} + + + + + 🟩 + + + + + + + + + Create Todo + + + + + {{ data }} + + + + + + + diff --git a/examples/subscription/nuxt.config.ts b/examples/subscription/nuxt.config.ts new file mode 100644 index 0000000..0bc5241 --- /dev/null +++ b/examples/subscription/nuxt.config.ts @@ -0,0 +1,9 @@ +export default defineNuxtConfig({ + modules: ['@nuxt/ui', 'nuxt-graphql-client'], + + runtimeConfig: { + public: { + GQL_HOST: 'https://nuxt-gql-server-2gl6xp7kua-ue.a.run.app/query' + } + } +}) diff --git a/examples/subscription/package.json b/examples/subscription/package.json new file mode 100644 index 0000000..ce39117 --- /dev/null +++ b/examples/subscription/package.json @@ -0,0 +1,16 @@ +{ + "private": true, + "name": "example-subscription", + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "preview": "nuxt preview", + "generate": "nuxt generate", + "postinstall": "nuxt prepare" + }, + "devDependencies": { + "nuxt": "latest", + "@nuxt/ui": "latest", + "nuxt-graphql-client": "latest" + } +} diff --git a/examples/subscription/queries/todos.gql b/examples/subscription/queries/todos.gql new file mode 100644 index 0000000..b2c40a6 --- /dev/null +++ b/examples/subscription/queries/todos.gql @@ -0,0 +1,7 @@ +query getTodos { todos { id text } } + +query getTodo($id: Int!) { todo (id: $id) { id text } } + +mutation createTodo($todo: TodoInput!) { createTodo(todo: $todo) { id text } } + +subscription todoAdded { todoAdded { id text } } diff --git a/examples/subscription/tsconfig.json b/examples/subscription/tsconfig.json new file mode 100644 index 0000000..0d9b5cb --- /dev/null +++ b/examples/subscription/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/package.json b/package.json index d3ff1e7..2394248 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "gql", "graphql", "graphql-client", - "graphql-request", + "ofetch", "codegen", "graphql-code-generator" ], @@ -47,13 +47,12 @@ "dependencies": { "@graphql-codegen/cli": "npm:graphql-codegen-cli-nuxt@latest", "@graphql-codegen/typescript": "^2.8.3", - "@graphql-codegen/typescript-graphql-request": "^4.5.8", "@graphql-codegen/typescript-operations": "^2.5.8", "@nuxt/kit": "latest", "defu": "^6.1.1", "graphql": "^16.6.0", - "graphql-request": "^5.0.0", "knitwork": "^1.0.0", + "ogql": "^0.0.1", "ohash": "^1.0.0", "scule": "^1.0.0" }, @@ -64,6 +63,7 @@ "@vitest/coverage-c8": "^0.25.3", "eslint": "^8.29.0", "nuxt": "latest", + "playwright": "^1.28.1", "vitest": "^0.25.3" }, "packageManager": "pnpm@7.18.0" diff --git a/playground/plugins/onError.ts b/playground/plugins/onError.ts index 8a29241..5f64703 100644 --- a/playground/plugins/onError.ts +++ b/playground/plugins/onError.ts @@ -5,9 +5,8 @@ export default defineNuxtPlugin(() => { for (const gqlError of err.gqlErrors) { console.error('[nuxt-graphql-client] [GraphQL error]', { client: err.client, + operation: err.operation, statusCode: err.statusCode, - operationType: err.operationType, - operationName: err.operationName, gqlError }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13eee40..8d00a6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ importers: specifiers: '@graphql-codegen/cli': npm:graphql-codegen-cli-nuxt@latest '@graphql-codegen/typescript': ^2.8.3 - '@graphql-codegen/typescript-graphql-request': ^4.5.8 '@graphql-codegen/typescript-operations': ^2.5.8 '@nuxt/kit': latest '@nuxt/module-builder': latest @@ -16,22 +15,22 @@ importers: defu: ^6.1.1 eslint: ^8.29.0 graphql: ^16.6.0 - graphql-request: ^5.0.0 knitwork: ^1.0.0 nuxt: latest + ogql: ^0.0.1 ohash: ^1.0.0 + playwright: ^1.28.1 scule: ^1.0.0 vitest: ^0.25.3 dependencies: '@graphql-codegen/cli': /graphql-codegen-cli-nuxt/0.0.0-patch.2.13.12-fix_graphql@16.6.0 '@graphql-codegen/typescript': 2.8.3_graphql@16.6.0 - '@graphql-codegen/typescript-graphql-request': 4.5.8_7lz647nkgovf4mzr2sitna7qte '@graphql-codegen/typescript-operations': 2.5.8_graphql@16.6.0 '@nuxt/kit': 3.0.0 defu: 6.1.1 graphql: 16.6.0 - graphql-request: 5.0.0_graphql@16.6.0 knitwork: 1.0.0 + ogql: 0.0.1 ohash: 1.0.0 scule: 1.0.0 devDependencies: @@ -41,6 +40,7 @@ importers: '@vitest/coverage-c8': 0.25.3 eslint: 8.29.0 nuxt: 3.0.0_eslint@8.29.0 + playwright: 1.28.1 vitest: 0.25.3 examples/basic: @@ -83,6 +83,16 @@ importers: nuxt: 3.0.0 nuxt-graphql-client: link:../.. + examples/subscription: + specifiers: + '@nuxt/ui': latest + nuxt: latest + nuxt-graphql-client: latest + devDependencies: + '@nuxt/ui': 0.3.3_nuxt@3.0.0 + nuxt: 3.0.0 + nuxt-graphql-client: link:../.. + playground: specifiers: nuxt-graphql-client: workspace:* @@ -762,6 +772,63 @@ packages: - supports-color dev: true + /@graphql-codegen/cli/2.13.12_graphql@16.6.0: + resolution: {integrity: sha512-9pr39oseKQyQvm1tRFvW/2kt8c5JmT8u+5X6FZVBqWE18l1g4hB+XOeUNg/oEBdeDfiP7bvYjtQYOZaToXz9IQ==} + hasBin: true + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + dependencies: + '@babel/generator': 7.20.5 + '@babel/template': 7.18.10 + '@babel/types': 7.20.5 + '@graphql-codegen/core': 2.6.6_graphql@16.6.0 + '@graphql-codegen/plugin-helpers': 2.7.2_graphql@16.6.0 + '@graphql-tools/apollo-engine-loader': 7.3.19_graphql@16.6.0 + '@graphql-tools/code-file-loader': 7.3.13_graphql@16.6.0 + '@graphql-tools/git-loader': 7.2.13_graphql@16.6.0 + '@graphql-tools/github-loader': 7.3.20_graphql@16.6.0 + '@graphql-tools/graphql-file-loader': 7.5.11_graphql@16.6.0 + '@graphql-tools/json-file-loader': 7.4.12_graphql@16.6.0 + '@graphql-tools/load': 7.8.0_graphql@16.6.0 + '@graphql-tools/prisma-loader': 7.2.42_graphql@16.6.0 + '@graphql-tools/url-loader': 7.16.22_graphql@16.6.0 + '@graphql-tools/utils': 8.13.1_graphql@16.6.0 + '@whatwg-node/fetch': 0.3.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + chokidar: 3.5.3 + cosmiconfig: 7.1.0 + cosmiconfig-typescript-loader: 4.1.1_cosmiconfig@7.1.0 + debounce: 1.2.1 + detect-indent: 6.1.0 + graphql: 16.6.0 + graphql-config: 4.3.6_graphql@16.6.0 + inquirer: 8.2.5 + is-glob: 4.0.3 + json-to-pretty-yaml: 1.2.2 + listr2: 4.0.5 + log-symbols: 4.1.0 + mkdirp: 1.0.4 + shell-quote: 1.7.4 + string-env-interpolation: 1.0.1 + ts-log: 2.2.5 + tslib: 2.4.1 + yaml: 1.10.2 + yargs: 17.6.2 + transitivePeerDependencies: + - '@babel/core' + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - bufferutil + - encoding + - enquirer + - supports-color + - ts-node + - typescript + - utf-8-validate + dev: false + /@graphql-codegen/core/2.6.6_graphql@16.6.0: resolution: {integrity: sha512-gU2FUxoLGw2GfcPWfBVXuiN3aDODbZ6Z9I+IGxa2u1Rzxlacw4TMmcwr4/IjC6mkiYJEKTvdVspHaby+brhuAg==} peerDependencies: @@ -799,24 +866,6 @@ packages: tslib: 2.4.1 dev: false - /@graphql-codegen/typescript-graphql-request/4.5.8_7lz647nkgovf4mzr2sitna7qte: - resolution: {integrity: sha512-XsuAA35Ou03LsklNgnIWXZ5HOHsJ5w1dBuDKtvqM9rD0cAI8x0f4TY0n6O1EraSBSvyHLP3npb1lOTPZzG2TjA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - graphql-request: ^3.4.0 || ^4.0.0 || ^5.0.0 - graphql-tag: ^2.0.0 - dependencies: - '@graphql-codegen/plugin-helpers': 2.7.2_graphql@16.6.0 - '@graphql-codegen/visitor-plugin-common': 2.13.1_graphql@16.6.0 - auto-bind: 4.0.0 - graphql: 16.6.0 - graphql-request: 5.0.0_graphql@16.6.0 - tslib: 2.4.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - /@graphql-codegen/typescript-operations/2.5.8_graphql@16.6.0: resolution: {integrity: sha512-Zp27jZjOLkoH0qy5INqrTsut5PI40OEVcKmcQ+TDHr9wDYa3M06/k907z6CuW3PjOgJBtrSTcgAEnrye8jhkJw==} peerDependencies: @@ -849,27 +898,6 @@ packages: - supports-color dev: false - /@graphql-codegen/visitor-plugin-common/2.13.1_graphql@16.6.0: - resolution: {integrity: sha512-mD9ufZhDGhyrSaWQGrU1Q1c5f01TeWtSWy/cDwXYjJcHIj1Y/DG2x0tOflEfCvh5WcnmHNIw4lzDsg1W7iFJEg==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - dependencies: - '@graphql-codegen/plugin-helpers': 2.7.2_graphql@16.6.0 - '@graphql-tools/optimize': 1.3.1_graphql@16.6.0 - '@graphql-tools/relay-operation-optimizer': 6.5.12_graphql@16.6.0 - '@graphql-tools/utils': 8.13.1_graphql@16.6.0 - auto-bind: 4.0.0 - change-case-all: 1.0.14 - dependency-graph: 0.11.0 - graphql: 16.6.0 - graphql-tag: 2.12.6_graphql@16.6.0 - parse-filepath: 1.0.2 - tslib: 2.4.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - /@graphql-codegen/visitor-plugin-common/2.13.3_graphql@16.6.0: resolution: {integrity: sha512-5gFDQGuCE5tIBo9KtDPZ8kL6cf1VJwDGj6nO9ERa0HJNk5osT50NhSf6H61LEnM3Gclbo96Ib1GCp3KdLwHoGg==} peerDependencies: @@ -6522,7 +6550,6 @@ packages: /node-fetch-native/1.0.1: resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==} - dev: true /node-fetch/2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} @@ -6814,7 +6841,33 @@ packages: destr: 1.2.1 node-fetch-native: 1.0.1 ufo: 1.0.1 - dev: true + + /ogql/0.0.1: + resolution: {integrity: sha512-2WWzAtRXJCpgoSf0pXHSgn+vcWQFbLbQB2+DnvI8i2Gs+BDe2Ly3MRSYNvgmx2CEOke3lOHhmc1azDRoRhCzeg==} + dependencies: + '@graphql-codegen/cli': 2.13.12_graphql@16.6.0 + '@graphql-codegen/plugin-helpers': 2.7.2_graphql@16.6.0 + '@graphql-codegen/typescript': 2.8.3_graphql@16.6.0 + '@graphql-codegen/typescript-operations': 2.5.8_graphql@16.6.0 + '@graphql-codegen/visitor-plugin-common': 2.13.3_graphql@16.6.0 + graphql: 16.6.0 + graphql-tag: 2.12.6_graphql@16.6.0 + graphql-ws: 5.11.2_graphql@16.6.0 + ofetch: 1.0.0 + scule: 1.0.0 + transitivePeerDependencies: + - '@babel/core' + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - bufferutil + - encoding + - enquirer + - supports-color + - ts-node + - typescript + - utf-8-validate + dev: false /ohash/1.0.0: resolution: {integrity: sha512-kxSyzq6tt+6EE/xCnD1XaFhCCjUNUaz3X30rJp6mnjGLXAAvuPFqohMdv0aScWzajR45C29HyBaXZ8jXBwnh9A==} @@ -7090,6 +7143,21 @@ packages: mlly: 1.0.0 pathe: 1.0.0 + /playwright-core/1.28.1: + resolution: {integrity: sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /playwright/1.28.1: + resolution: {integrity: sha512-92Sz6XBlfHlb9tK5UCDzIFAuIkHHpemA9zwUaqvo+w7sFMSmVMGmvKcbptof/eJObq63PGnMhM75x7qxhTR78Q==} + engines: {node: '>=14'} + hasBin: true + requiresBuild: true + dependencies: + playwright-core: 1.28.1 + dev: true + /pluralize/8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} diff --git a/src/context.ts b/src/context.ts index 2e9998d..ebb948f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -23,7 +23,7 @@ export async function prepareContext (ctx: GqlContext, prefix: string) { if (ctx.template) { prepareTemplate(ctx) } ctx.fns = Object.values(ctx.template || {}).reduce((acc, template) => { - const fns = template.match(ctx?.codegen ? /\w+\s*(?=\(variables)/g : /\w+(?=:\s\(variables)/g)?.sort() || [] + const fns = template.match(!ctx?.codegen ? /\w+(?=:\s\(variables)/g : /\w+(?=:\s [ 'import { useGql } from \'#imports\'', - ...ctx.clients!.map(client => `import { getSdk as ${client}GqlSdk } from '#gql/${client}'`), + ...ctx.clients.map(client => `import { gqlSdk as ${client}GqlSdk } from '#gql/${client}'`), 'export const GqlSdks = {', ...ctx.clients!.map(client => ` ${client}: ${client}GqlSdk,`), '}', @@ -53,7 +53,7 @@ export async function prepareContext (ctx: GqlContext, prefix: string) { ctx.generateDeclarations = () => [ ...(!ctx.codegen ? [] - : ctx.clients!.map(client => `import { getSdk as ${client}GqlSdk } from '#gql/${client}'`)), + : ctx.clients.map(client => `import { gqlSdk as ${client}GqlSdk } from '#gql/${client}'`)), ...Object.entries(ctx.clientTypes || {}).map(([k, v]) => genExport(`#gql/${k}`, v)), 'declare module \'#gql\' {', ` type GqlClients = '${ctx.clients?.join("' | '") || 'default'}'`, @@ -114,19 +114,3 @@ function prepareTemplate (ctx: GqlContext) { return { ...acc, [key]: results } }, {} as Record) } - -export const mockTemplate = (operations: Record) => { - const GqlFunctions: string[] = [] - - for (const [k, v] of Object.entries(operations)) { - GqlFunctions.push(` ${k}: (variables = undefined, requestHeaders = undefined) => withWrapper((wrappedRequestHeaders) => client.request(\`${v}\`, variables, {...requestHeaders, ...wrappedRequestHeaders}), '${k}', 'query')`) - } - - return [ - 'export function getSdk(client, withWrapper = (action, _operationName, _operationType) => action()) {', - ' return {', - GqlFunctions.join(',\n'), - ' }', - '}' - ].join('\n') -} diff --git a/src/generate.ts b/src/generate.ts index 2619eea..9721376 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -44,7 +44,6 @@ function prepareConfig (options: GenerateOptions & GqlCodegen): CodegenConfig { skipTypename: options?.skipTypename, useTypeImports: options?.useTypeImports, dedupeFragments: options?.dedupeFragments, - gqlImport: 'graphql-request#gql', onlyOperationTypes: options.onlyOperationTypes, namingConvention: { enumValues: 'change-case-all#upperCaseFirst' diff --git a/src/module.ts b/src/module.ts index e4a7987..d42ab94 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,12 +1,13 @@ import { existsSync, statSync } from 'fs' import { defu } from 'defu' import { upperFirst } from 'scule' -import { useLogger, addPlugin, addImportsDir, addTemplate, resolveFiles, createResolver, defineNuxtModule, extendViteConfig } from '@nuxt/kit' +import { mockPlugin } from 'ogql/plugin' +import { useLogger, addPlugin, addImportsDir, addTemplate, resolveFiles, createResolver, defineNuxtModule } from '@nuxt/kit' import { name, version } from '../package.json' import generate from './generate' import { mapDocsToClients, extractGqlOperations } from './utils' import type { GqlConfig, GqlClient, GqlCodegen, TokenStorageOpts } from './types' -import { prepareContext, mockTemplate } from './context' +import { prepareContext } from './context' import type { GqlContext } from './context' const logger = useLogger('nuxt-graphql-client') @@ -170,7 +171,7 @@ export default defineNuxtModule({ if (documents?.length) { ctx.clientDocs = mapDocsToClients(documents, ctx.clients!) - plugins.push('typescript-operations', 'typescript-graphql-request') + plugins.push('typescript-operations', 'ogql/plugin') } if (ctx.clientDocs) { @@ -194,7 +195,7 @@ export default defineNuxtModule({ const entries = extractGqlOperations(ctx?.clientDocs?.[k] || []) - return { ...acc, [k]: mockTemplate(entries) } + return { ...acc, [k]: mockPlugin(entries) } }, {}) ctx.template = defu(codegenResult, ctx.template) @@ -244,7 +245,7 @@ export default defineNuxtModule({ const clientSdks = Object.entries(ctx.clientDocs || {}).reduce((acc, [client, docs]) => { const entries = extractGqlOperations(docs) - return [...acc, `${client}: ` + mockTemplate(entries).replace('export ', '')] + return [...acc, `${client}: ` + mockPlugin(entries).replace('export ', '')] }, []) nitro.virtual = nitro.virtual || {} @@ -294,10 +295,6 @@ export default defineNuxtModule({ } await generateGqlTypes() - - extendViteConfig((config) => { - config.optimizeDeps?.include?.push('graphql-request') - }) } }) diff --git a/src/runtime/composables/index.ts b/src/runtime/composables/index.ts index 0952372..42ac569 100644 --- a/src/runtime/composables/index.ts +++ b/src/runtime/composables/index.ts @@ -2,8 +2,8 @@ import { defu } from 'defu' import { hash } from 'ohash' import { isRef, reactive } from 'vue' import type { Ref } from 'vue' +import { extractOperation } from 'ogql/utils' import type { AsyncData } from 'nuxt/dist/app/composables' -import type { ClientError } from 'graphql-request' import type { GqlState, GqlConfig, GqlError, TokenOpts, OnGqlError, GqlStateOpts } from '../../types' // @ts-ignore import { GqlSdks, GqClientOps } from '#gql' @@ -195,7 +195,7 @@ export const useGqlHost = (host: string, client?: GqlClients) => { host = `${initialHost}${host}` } - return state.value?.[client].instance!.setEndpoint(host) + state.value?.[client].instance!.setHost(host) } export const useGql = (): (< @@ -219,27 +219,22 @@ export const useGql = (): (< const { instance } = state!.value?.[client] - if (!instance) { throw new Error('Invalid GraphQL Operation') } - - return GqlSdks[client as keyof typeof GqlSdks]!(instance, async (action, operationName, operationType): Promise => { - try { - return await action() - } catch (err: ClientError | any) { + instance.setMiddleware({ + onResponseError: ({ options, response }) => { errState.value = { client, - operationType, - operationName, - statusCode: err?.response?.status, - gqlErrors: err?.response?.errors || (err?.response?.message && [{ message: err?.response?.message }]) || [] + statusCode: response?.status, + operation: extractOperation(JSON.parse(options?.body as string)?.query || ''), + gqlErrors: Array.isArray(response?._data?.errors) ? response?._data?.errors : [response?._data] } if (state.value.onError) { state.value.onError(errState.value) } - - throw errState.value } - })[operation as GqlOps](variables) as any + }) + + return !args?.[2] ? GqlSdks[client]?.(instance)[operation](variables) : GqlSdks[client]?.(instance)[operation](variables, args?.[2]) } } diff --git a/src/runtime/nitro.ts b/src/runtime/nitro.ts index 8072d3b..5493ece 100644 --- a/src/runtime/nitro.ts +++ b/src/runtime/nitro.ts @@ -1,4 +1,4 @@ -import { GraphQLClient } from 'graphql-request' +import { GqlClient } from 'ogql' import type { GqlConfig } from '../types' // @ts-ignore import { defineNitroPlugin } from '#internal/nitro' @@ -22,6 +22,6 @@ export default defineNitroPlugin(() => { ...(conf?.token?.value && { [tokenName]: authToken }) } - GqlNitro.clients[client] = new GraphQLClient(conf.host, { headers }) + GqlNitro.clients[client] = GqlClient({ host: conf.host, headers }) } }) diff --git a/src/runtime/plugin.ts b/src/runtime/plugin.ts index 69874e7..8e190a3 100644 --- a/src/runtime/plugin.ts +++ b/src/runtime/plugin.ts @@ -1,7 +1,7 @@ import { defu } from 'defu' import type { Ref } from 'vue' -import { GraphQLClient } from 'graphql-request' -import type { GqlState, GqlConfig } from '../types' +import { GqlClient } from 'ogql' +import type { GqlState, GqlConfig, GqlStateOpts } from '../types' import { ref, useCookie, useNuxtApp, defineNuxtPlugin, useRuntimeConfig, useRequestHeaders } from '#imports' import type { GqlClients } from '#gql' @@ -40,55 +40,70 @@ export default defineNuxtPlugin((nuxtApp) => { ...v?.corsOptions } - nuxtApp._gqlState.value[name] = { - options: opts, - instance: new GraphQLClient(host!, { - ...(v?.preferGETQueries && { - method: 'GET', - jsonSerializer: { parse: JSON.parse, stringify: JSON.stringify } - }), - requestMiddleware: async (req) => { - const token = ref() - await nuxtApp.callHook('gql:auth:init', { token, client: name as GqlClients }) - - const reqOpts = defu(nuxtApp._gqlState.value?.[name]?.options || {}, { headers: {} }) - - if (!token.value) { token.value = reqOpts?.token?.value } - - if (token.value === undefined && typeof v.tokenStorage === 'object') { - if (v.tokenStorage?.mode === 'cookie') { - if (process.client) { - token.value = useCookie(v.tokenStorage.name!).value - } else if (cookie) { - const cookieName = `${v.tokenStorage.name}=` - token.value = cookie.split(';').find(c => c.trim().startsWith(cookieName))?.split('=')?.[1] - } - } else if (process.client && v.tokenStorage?.mode === 'localStorage') { - const storedToken = localStorage.getItem(v.tokenStorage.name!) - - if (storedToken) { token.value = storedToken } - } + const authInit = async (reqOpts: GqlStateOpts['options']) => { + const token = ref() + await nuxtApp.callHook('gql:auth:init', { token, client: name as GqlClients }) + + if (!token.value) { token.value = reqOpts?.token?.value } + + if (token.value === undefined && typeof v.tokenStorage === 'object') { + if (v.tokenStorage?.mode === 'cookie') { + if (process.client) { + token.value = useCookie(v.tokenStorage.name!).value + } else if (cookie) { + const cookieName = `${v.tokenStorage.name}=` + token.value = cookie.split(';').find(c => c.trim().startsWith(cookieName))?.split('=')?.[1] } + } else if (process.client && v.tokenStorage?.mode === 'localStorage') { + const storedToken = localStorage.getItem(v.tokenStorage.name!) - if (token.value === undefined) { token.value = v?.token?.value } + if (storedToken) { token.value = storedToken } + } + } - if (token.value) { - token.value = token.value.trim() + if (token.value === undefined) { token.value = v?.token?.value } - const tokenName = token.value === reqOpts?.token?.value ? reqOpts?.token?.name || v?.token?.name : v?.token?.name - const tokenType = token.value === reqOpts?.token?.value ? reqOpts?.token?.type === null ? null : reqOpts?.token?.type || v?.token?.type : v?.token?.type + if (token.value) { + token.value = token.value.trim() - const authScheme = !!token.value?.match(/^[a-zA-Z]+\s/)?.[0] + const tokenName = token.value === reqOpts?.token?.value ? reqOpts?.token?.name || v?.token?.name : v?.token?.name + const tokenType = token.value === reqOpts?.token?.value ? reqOpts?.token?.type === null ? null : reqOpts?.token?.type || v?.token?.type : v?.token?.type - if (authScheme) { - reqOpts.headers[tokenName] = token.value - } else { - reqOpts.headers[tokenName] = !tokenType ? token.value : `${tokenType} ${token.value}` - } + const authScheme = !!token.value?.match(/^[a-zA-Z]+\s/)?.[0] + + reqOpts.headers[tokenName] = authScheme ? token.value : !tokenType ? token.value : `${tokenType} ${token.value}` + + return { name: tokenName, token: reqOpts.headers[tokenName] as string } + } + + return undefined + } + + nuxtApp._gqlState.value[name] = { + options: opts, + instance: GqlClient({ + host, + useGETForQueries: v?.preferGETQueries, + middleware: { + onRequest: async (ctx) => { + const reqOpts = defu(nuxtApp._gqlState.value?.[name]?.options || {}, { headers: {} }) + + await authInit(reqOpts) + + if (reqOpts?.token) { delete reqOpts.token } + ctx.options = defu(ctx.options, reqOpts) } + }, + wsOptions: { + connectionParams: async () => { + const reqOpts = defu(nuxtApp._gqlState.value?.[name]?.options || {}, { headers: {} }) + + const token = await authInit(reqOpts) - if (reqOpts?.token) { delete reqOpts.token } - return defu(req, reqOpts) + if (!token) { return } + + return { [token.name]: token.token } + } } }) } diff --git a/src/types.d.ts b/src/types.d.ts index 0970e36..e89df67 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,5 +1,4 @@ -import type { GraphQLClient } from 'graphql-request' -import type { GraphQLError } from 'graphql-request/dist/types' +import type { GqlClient as GQLClient, GraphQLError, GqlOperation } from 'ogql' import type { CookieOptions } from 'nuxt/dist/app/composables' type TokenOpts = { @@ -243,14 +242,13 @@ export interface GqlConfig { } export type GqlError = { - client: string - operationName?: string - operationType?: string + client?: string statusCode?: number + operation?: GqlOperation gqlErrors: GraphQLError[] } export type OnGqlError = (error: GqlError) => Promise | any -type GqlStateOpts = {instance?: GraphQLClient, options?: { token?: TokenOpts } & Pick } +type GqlStateOpts = {instance?: GQLClient, options?: { token?: TokenOpts } & Pick } export type GqlState = Record & { onError?: OnGqlError } diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..87d4ead --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,18 @@ +import { $fetch } from 'ofetch' + +await Promise.all([ + // ensure rick and morty api is ready + $fetch('https://rickandmortyapi.com'), + + // ensure todo api is ready + $fetch('https://nuxt-gql-server-2gl6xp7kua-ue.a.run.app'), + + // ensure spacex api is ready + $fetch('https://spacex-api-2gl6xp7kua-ue.a.run.app'), + + // ensure country api is ready + $fetch('https://countries.trevorblades.com', { + method: 'post', + body: { query: '{ continents { name } }' } + }) +]) diff --git a/test/subscription.test.ts b/test/subscription.test.ts new file mode 100644 index 0000000..b6cfe6b --- /dev/null +++ b/test/subscription.test.ts @@ -0,0 +1,30 @@ +import { fileURLToPath } from 'node:url' +import { describe, it, expect } from 'vitest' +import { setup, createPage } from '@nuxt/test-utils' + +await setup({ + server: true, + browser: true, + rootDir: fileURLToPath(new URL('../examples/subscription', import.meta.url)) +}) + +describe('subscription tests', () => { + it('subscribe to websocket', async () => { + const page = await createPage('/') + + await page.waitForSelector('#connected') + + const randomId = Math.random().toString() + + await page.fill('#todoInput', randomId) + + await Promise.all([ + page.click('text=Create Todo'), + page.waitForResponse('https://nuxt-gql-server-2gl6xp7kua-ue.a.run.app/query') + ]) + + const response = await page.innerText('#todoResponse') + + expect(JSON.parse(response)).toContain({ text: randomId }) + }, 15000) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1fc1c6c --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + setupFiles: ['./test/setup'] + } +})
WS Status: {{ wsStatus }}
+ 🟩 +