@lewebsimple/nuxt-graphql 0.2.1 โ†’ 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,80 +5,110 @@
5
5
  [![License][license-src]][license-href]
6
6
  [![Nuxt][nuxt-src]][nuxt-href]
7
7
 
8
- Opinionated Nuxt module for using GraphQL Yoga with graphql-request / graphql-sse.
8
+ Opinionated Nuxt module that ships a stitched GraphQL Yoga server, typed GraphQL Codegen, and client/server composables powered by graphql-request and graphql-sse.
9
9
 
10
10
  - โœจ [Release Notes](/CHANGELOG.md)
11
11
  - ๐Ÿ€ [Online playground](https://stackblitz.com/github/lewebsimple/nuxt-graphql?file=playground%2Fapp.vue)
12
12
 
13
13
  ## Features
14
- - ๐Ÿง˜โ€โ™‚๏ธ GraphQL Yoga server handler with user-provided schema / context
15
- - ๐Ÿ“„ Auto-import GraphQL documents from `**/*.gql` (configurable)
16
- - ๐Ÿงฉ Type-safe composables to call operations by name, i.e. `useGraphQLQuery("Hello")`
17
- - ๐Ÿงต Optional stitching of local schema with remote schemas (custom headers), with stitched SDL emitted to `server/graphql/schema.graphql`
18
14
 
19
- ## Quick Setup
15
+ - ๐Ÿง˜โ€โ™‚๏ธ GraphQL Yoga handler at `/api/graphql` with GraphiQL in development.
16
+ - ๐Ÿชก Schema stitching: mix local schemas and remote endpoints (with per-source headers). Stitched SDL is emitted to `server/graphql/schema.graphql` (configurable).
17
+ - ๐Ÿšฆ Remote middleware hooks: per-remote `onRequest` / `onResponse` callbacks to tweak headers, log responses, or short-circuit requests before forwarding.
18
+ - ๐Ÿช„ Code generation: scans named operations in `**/*.gql` (across Nuxt layers), generates typed documents, operations, registry (`#graphql/registry`), and optional Zod validation.
19
+ - ๐Ÿงฉ Typed composables: `useGraphQLQuery`, `useGraphQLMutation`, `useGraphQLSubscription` consume registry names (e.g. `useGraphQLQuery("Hello")`). Server equivalents mirror the API for Nitro handlers.
20
+ - ๐Ÿš€ Caching and dedupe: in-memory or localStorage TTL cache, in-flight request deduplication, and refresh callbacks driven by runtime config.
21
+ - ๐Ÿ“ก SSE subscriptions: client-only via graphql-sse, using the same registry documents.
22
+ - ๐Ÿ›ก๏ธSSR-friendly clients: forward `cookie` and `authorization` headers automatically on the server.
23
+
24
+ ## Quick start
20
25
 
21
26
  Install the module to your Nuxt application with one command:
22
27
 
23
28
  ```bash
24
- npx nuxi module add @lewebsimple/nuxt-graphql
29
+ pnpx nuxi module add @lewebsimple/nuxt-graphql
25
30
  ```
26
31
 
27
- Optionnally adjust options in your Nuxt config. The defaults shown below:
32
+ Configure at least one schema (local or remote) and optionnally your context (path to your context factory). Example with a local schema and remote stitched source:
28
33
 
29
34
  ```ts
30
35
  // nuxt.config.ts
31
36
  export default defineNuxtConfig({
32
37
  modules: ["@lewebsimple/nuxt-graphql"],
33
38
  graphql: {
34
- // Codegen controls document scanning and outputs
39
+ // Optional: path to your GraphQL context factory
40
+ // Defaults to src/runtime/server/lib/default-context.ts if omitted
41
+ context: "server/graphql/context.ts",
42
+ schemas: {
43
+ local: { type: "local", path: "server/graphql/schema.ts" },
44
+ swapi: {
45
+ type: "remote",
46
+ url: "https://swapi-graphql.netlify.app/.netlify/functions/index",
47
+ middleware: "server/graphql/swapi-middleware.ts",
48
+ },
49
+ },
35
50
  codegen: {
36
- enabled: true,
37
- pattern: "**/*.gql", // scan .gql files across layers
38
- schemaOutput: "server/graphql/schema.graphql", // saved SDL
51
+ documents: "**/*.gql", // only named operations allowed
52
+ saveSchema: "server/graphql/schema.graphql",
39
53
  },
40
- },
41
- });
42
- ```
43
-
44
- Define your GraphQL schema in `server/graphql/schema.ts`:
45
-
46
- ```ts
47
- import { createSchema } from "graphql-yoga";
48
- import type { GraphQLContext } from "./context";
49
-
50
- export const schema = createSchema<GraphQLContext>({
51
- typeDefs: /* GraphQL */ `
52
- type Query {
53
- hello: String!
54
- }
55
- `,
56
- resolvers: {
57
- Query: {
58
- hello: () => "Hello world!",
54
+ client: {
55
+ cache: { enabled: true, ttl: 60_000, storage: "memory" },
56
+ headers: {},
59
57
  },
60
58
  },
61
59
  });
62
60
  ```
63
61
 
64
- Optionnally define your GraphQL context in `server/graphql/context.ts`:
62
+ Define context (optional) in `server/graphql/context.ts`:
65
63
 
66
64
  ```ts
67
65
  import type { H3Event } from "h3";
68
66
 
69
- export async function createContext(_event: H3Event) {
70
- return {
71
- foo: "bar",
72
- };
67
+ export async function createContext(event: H3Event) {
68
+ return { event, user: event.context.user };
73
69
  }
70
+ ```
71
+
72
+ Write named operations in `.gql` files and use the auto-generated composables by operation name:
74
73
 
75
- export type GraphQLContext = Awaited<ReturnType<typeof createContext>>;
74
+ ```ts
75
+ const { data, pending, error } = useGraphQLQuery("Hello", { name: "world" });
76
+ const { mutate } = useGraphQLMutation("Ping");
77
+ const { data: time } = useGraphQLSubscription("Time");
76
78
  ```
77
79
 
78
80
  That's it! You can now use Nuxt GraphQL in your Nuxt app โœจ
79
81
 
80
82
  Yoga GraphiQL is available at `http://localhost:3000/api/graphql` by default.
81
83
 
84
+ Optional: add a remote middleware at `server/graphql/swapi-middleware.ts` to adjust headers or log activity for stitched sources:
85
+
86
+ ```ts
87
+ export default {
88
+ async onRequest({ fetchOptions }) {
89
+ return {
90
+ ...fetchOptions,
91
+ headers: {
92
+ ...fetchOptions.headers,
93
+ "x-swapi-api-key": process.env.SWAPI_TOKEN ?? "",
94
+ },
95
+ };
96
+ },
97
+ async onResponse({ operationName }) {
98
+ console.log(`[SWAPI] completed ${operationName ?? "unknown"}`);
99
+ },
100
+ } satisfies RemoteMiddleware
101
+ ```
102
+
103
+ Both hooks are optional; return a new `RequestInit` from `onRequest` to override the outgoing fetch, or use `onResponse` for side-effects such as metrics and logging.
104
+
105
+ ## Development notes
106
+
107
+ - Generated artifacts live under `.nuxt/graphql` and `.graphqlrc`; they are rewritten only when contents change.
108
+ - Operations must be **named and unique**; duplicates or unnamed operations fail codegen.
109
+ - SSE subscriptions are client-only; do not call `$graphqlSSE` on the server.
110
+ - Cache defaults come from `runtimeConfig.public.graphql.cache`; pass `cache: false` to per-call options to bypass.
111
+
82
112
  ## Contribution
83
113
 
84
114
  <details>
package/dist/module.d.mts CHANGED
@@ -9,6 +9,7 @@ type RemoteSchema = {
9
9
  type: "remote";
10
10
  url: string;
11
11
  headers?: Record<string, string>;
12
+ middleware?: string;
12
13
  };
13
14
  type SchemaDefinition = LocalSchema | RemoteSchema;
14
15
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
3
  "configKey": "graphql",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -185,7 +185,7 @@ function formatDefinitions(defs) {
185
185
  };
186
186
  return defs.map((def) => `${colorOf(def)}${def.name}${reset}`).join(`${dim} / ${reset}`);
187
187
  }
188
- function writeRegistryModule(registryPath, { operationsByType }) {
188
+ function writeRegistryModule({ registryPath, operationsByType }) {
189
189
  const queries = operationsByType.query.map((o) => o.name);
190
190
  const mutations = operationsByType.mutation.map((o) => o.name);
191
191
  const subscriptions = operationsByType.subscription.map((o) => o.name);
@@ -250,10 +250,7 @@ function writeRegistryModule(registryPath, { operationsByType }) {
250
250
  return writeFileIfChanged(registryPath, content.join("\n") + "\n");
251
251
  }
252
252
 
253
- const schemaHeader = "/* GraphQL */";
254
- const escapeSDL = (sdl) => sdl.replace(/`/g, "\\`");
255
- const serializeSDL = (sdl) => `${schemaHeader} \`${escapeSDL(sdl)}\``;
256
- function writeLocalSchemaModule(localPath, modulePath) {
253
+ function writeLocalSchemaModule({ localPath, modulePath }) {
257
254
  if (!existsSync(localPath)) {
258
255
  throw new Error(`Local schema file not found at path: ${localPath}`);
259
256
  }
@@ -262,7 +259,7 @@ function writeLocalSchemaModule(localPath, modulePath) {
262
259
  ].join("\n");
263
260
  return writeFileIfChanged(modulePath, content);
264
261
  }
265
- async function writeRemoteSchemaSdl({ url, headers }, sdlPath) {
262
+ async function writeRemoteSchemaSdl({ schemaDef: { url, headers }, sdlPath }) {
266
263
  const response = await fetch(url, {
267
264
  method: "POST",
268
265
  headers: { "Content-Type": "application/json", ...headers },
@@ -274,28 +271,42 @@ async function writeRemoteSchemaSdl({ url, headers }, sdlPath) {
274
271
  }
275
272
  const schema = buildClientSchema(json.data);
276
273
  const sdl = printSchema(schema);
277
- const content = [`export const sdl = ${serializeSDL(sdl)};`, ""].join("\n");
274
+ const content = `export const sdl = /* GraphQL */ \`${sdl.replace(/`/g, "\\`")}\`;`;
278
275
  return writeFileIfChanged(sdlPath, content);
279
276
  }
280
- function writeRemoteSchemaModule({ url, headers }, remoteSdlPath, modulePath) {
277
+ function writeRemoteSchemaModule({ name, schemaDef: { url, headers }, sdlPath, modulePath, middlewarePath }) {
281
278
  const headerSource = headers && Object.keys(headers).length > 0 ? JSON.stringify(headers, null, 2) : "{}";
279
+ const middlewareImport = middlewarePath ? `import middleware from ${JSON.stringify(toImportPath(modulePath, middlewarePath))};` : "";
282
280
  const content = [
283
281
  `import { buildSchema, print } from "graphql";`,
284
282
  `import type { Executor } from "@graphql-tools/utils";`,
285
283
  `import type { SubschemaConfig } from "@graphql-tools/delegate";`,
286
- `import { sdl } from ${JSON.stringify(toImportPath(modulePath, remoteSdlPath))};`,
284
+ `import { sdl } from ${JSON.stringify(toImportPath(modulePath, sdlPath))};`,
285
+ middlewareImport,
287
286
  ``,
288
287
  `const endpoint = ${JSON.stringify(url)};`,
289
288
  `const headers = ${headerSource} as Record<string, string>;`,
289
+ `const remoteName = ${JSON.stringify(name)};`,
290
+ `const mw = (typeof middleware === 'object' && middleware) || {};`,
290
291
  ``,
291
- `const executor: Executor = async ({ document, variables }) => {`,
292
+ `const executor: Executor = async ({ document, variables, context, operationName }) => {`,
292
293
  ` const query = typeof document === "string" ? document : print(document);`,
293
- ` const response = await fetch(endpoint, {`,
294
+ ` let fetchOptions = {`,
294
295
  ` method: "POST",`,
295
296
  ` headers: { "Content-Type": "application/json", ...headers },`,
296
297
  ` body: JSON.stringify({ query, variables }),`,
297
- ` });`,
298
- ``,
298
+ ` };`,
299
+ ` const mwContext = { remoteName, operationName, context, fetchOptions };`,
300
+ ` if (typeof mw.onRequest === "function") {`,
301
+ ` const maybeOverride = await mw.onRequest(mwContext);`,
302
+ ` if (maybeOverride && typeof maybeOverride === "object") {`,
303
+ ` fetchOptions = maybeOverride;`,
304
+ ` }`,
305
+ ` }`,
306
+ ` const response = await fetch(endpoint, fetchOptions);`,
307
+ ` if (typeof mw.onResponse === "function") {`,
308
+ ` await mw.onResponse({ ...mwContext, response });`,
309
+ ` }`,
299
310
  ` return response.json();`,
300
311
  `};`,
301
312
  ``,
@@ -307,7 +318,7 @@ function writeRemoteSchemaModule({ url, headers }, remoteSdlPath, modulePath) {
307
318
  ].join("\n");
308
319
  return writeFileIfChanged(modulePath, content);
309
320
  }
310
- function writeStitchedSchemaModule(schemaNames, modulePath) {
321
+ function writeStitchedSchemaModule({ schemaNames, modulePath }) {
311
322
  const schemas = schemaNames.map((name) => ({
312
323
  path: `./schemas/${name}`,
313
324
  ref: `${name}Schema`
@@ -351,44 +362,50 @@ const module$1 = defineNuxtModule({
351
362
  async setup(options, nuxt) {
352
363
  const { resolve } = createResolver(import.meta.url);
353
364
  const layerRootDirs = getLayerDirectories(nuxt).map(({ root }) => root);
365
+ const middlewarePath = resolve("./runtime/server/utils/remote-middleware");
354
366
  const stitchedPath = join(nuxt.options.buildDir, "graphql/schema.ts");
355
367
  const sdlPath = join(nuxt.options.rootDir, options.codegen?.saveSchema || ".nuxt/graphql/schema.graphql");
356
368
  nuxt.options.alias ||= {};
357
- if (!Object.keys(options.schemas || {}).length) {
358
- throw new Error("No GraphQL schemas configured. Please set 'graphql.schemas' with at least one local or remote schema.");
359
- }
360
369
  async function setupContextSchemas() {
361
370
  let contextPath;
362
371
  if (options.context) {
363
372
  contextPath = await findSingleFile(layerRootDirs, options.context, true);
364
373
  logger.info(`Using GraphQL context from ${cyan}${relative(nuxt.options.rootDir, contextPath)}${reset}`);
365
374
  } else {
366
- contextPath = resolve("./runtime/server/graphql/context");
375
+ contextPath = resolve("./runtime/server/lib/default-context.ts");
367
376
  logger.info(`Using default GraphQL context`);
368
377
  }
369
378
  const schemasPath = {};
379
+ const middlewaresPath = {};
370
380
  for (const [name, schemaDef] of Object.entries(options.schemas)) {
371
381
  schemasPath[name] = join(nuxt.options.buildDir, `graphql/schemas/${name}.ts`);
372
382
  if (schemaDef.type === "local") {
373
383
  const localPath = await findSingleFile(layerRootDirs, schemaDef.path, true);
374
- writeLocalSchemaModule(localPath, schemasPath[name]);
384
+ writeLocalSchemaModule({ localPath, modulePath: schemasPath[name] });
375
385
  logger.info(`Local GraphQL schema "${blue}${name}${reset}" loaded from ${cyan}${relative(nuxt.options.rootDir, localPath)}${reset}`);
376
386
  } else if (schemaDef.type === "remote") {
377
- const remoteSdlPath = join(nuxt.options.buildDir, `graphql/schemas/${name}-sdl.ts`);
378
- await writeRemoteSchemaSdl(schemaDef, remoteSdlPath);
379
- writeRemoteSchemaModule(schemaDef, remoteSdlPath, schemasPath[name]);
387
+ const sdlPath2 = join(nuxt.options.buildDir, `graphql/schemas/${name}-sdl.ts`);
388
+ if (schemaDef.middleware) {
389
+ middlewaresPath[name] = await findSingleFile(layerRootDirs, schemaDef.middleware, true);
390
+ }
391
+ await writeRemoteSchemaSdl({ schemaDef, sdlPath: sdlPath2 });
392
+ writeRemoteSchemaModule({ name, schemaDef, sdlPath: sdlPath2, modulePath: schemasPath[name], middlewarePath: middlewaresPath[name] });
380
393
  logger.info(`Remote GraphQL schema "${magenta}${name}${reset}" loaded from ${cyan}${schemaDef.url}${reset}`);
381
394
  } else {
382
395
  throw new Error(`Unknown schema type for schema '${name}'`);
383
396
  }
384
397
  }
385
- writeStitchedSchemaModule(Object.keys(options.schemas), stitchedPath);
398
+ writeStitchedSchemaModule({ schemaNames: Object.keys(options.schemas), modulePath: stitchedPath });
386
399
  nuxt.hook("nitro:config", (config) => {
387
400
  config.alias ||= {};
388
401
  config.alias["#graphql/context"] = contextPath;
402
+ config.alias["#graphql/middleware"] = middlewarePath;
389
403
  for (const name of Object.keys(options.schemas)) {
390
404
  config.alias[`#graphql/schemas/${name}`] = schemasPath[name];
391
405
  }
406
+ for (const name of Object.keys(middlewaresPath)) {
407
+ config.alias[`#graphql/middlewares/${name}`] = middlewaresPath[name];
408
+ }
392
409
  config.alias["#graphql/schema"] = stitchedPath;
393
410
  });
394
411
  }
@@ -404,12 +421,12 @@ const module$1 = defineNuxtModule({
404
421
  const documentsPattern = options.codegen?.documents ?? "**/*.gql";
405
422
  const documents = await findMultipleFiles(layerRootDirs, documentsPattern);
406
423
  await runCodegen({ schema: sdlPath, documents, operationsPath, zodPath, scalars: options.codegen?.scalars });
407
- const analysis = analyzeDocuments(documents);
408
- analysis.byFile.forEach((defs, path) => {
424
+ const { byFile, operationsByType } = analyzeDocuments(documents);
425
+ byFile.forEach((defs, path) => {
409
426
  const relativePath = relative(nuxt.options.rootDir, path);
410
427
  logger.info(`${cyan}${relativePath}${reset} [${formatDefinitions(defs)}]`);
411
428
  });
412
- writeRegistryModule(registryPath, analysis);
429
+ writeRegistryModule({ registryPath, operationsByType });
413
430
  const config = {
414
431
  schema: relative(nuxt.options.rootDir, sdlPath),
415
432
  documents: documentsPattern
@@ -1,9 +1,10 @@
1
1
  import { defineEventHandler, toWebRequest, sendWebResponse, createError } from "h3";
2
2
  import { getYoga } from "../lib/create-yoga.js";
3
+ import { createContext } from "#graphql/context";
3
4
  export default defineEventHandler(async (event) => {
4
5
  try {
5
6
  const request = toWebRequest(event);
6
- const context = {};
7
+ const context = await createContext(event);
7
8
  const response = await getYoga().handleRequest(request, context);
8
9
  return sendWebResponse(event, response);
9
10
  } catch (error) {
@@ -0,0 +1,18 @@
1
+ import type { GraphQLContext } from "#graphql/context";
2
+ type Awaitable<T> = T | Promise<T>;
3
+ export type RemoteMiddlewareContext = {
4
+ remoteName: string;
5
+ operationName?: string | null;
6
+ context: GraphQLContext;
7
+ };
8
+ export type RemoteMiddlewareRequestContext = RemoteMiddlewareContext & {
9
+ fetchOptions: RequestInit;
10
+ };
11
+ export type RemoteMiddlewareResponseContext = RemoteMiddlewareRequestContext & {
12
+ response: Response;
13
+ };
14
+ export type RemoteMiddleware = {
15
+ onRequest?: (context: RemoteMiddlewareRequestContext) => Awaitable<RequestInit | undefined>;
16
+ onResponse?: (context: RemoteMiddlewareResponseContext) => Awaitable<void>;
17
+ };
18
+ export {};
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Opinionated Nuxt module for using GraphQL",
5
5
  "repository": "lewebsimple/nuxt-graphql",
6
6
  "license": "MIT",