@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 +67 -37
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +43 -26
- package/dist/runtime/server/api/graphql-handler.js +2 -1
- package/dist/runtime/server/utils/remote-middleware.d.ts +18 -0
- package/dist/runtime/server/utils/remote-middleware.js +0 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
pnpx nuxi module add @lewebsimple/nuxt-graphql
|
|
25
30
|
```
|
|
26
31
|
|
|
27
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
package/dist/module.json
CHANGED
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,
|
|
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
|
-
|
|
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 =
|
|
274
|
+
const content = `export const sdl = /* GraphQL */ \`${sdl.replace(/`/g, "\\`")}\`;`;
|
|
278
275
|
return writeFileIfChanged(sdlPath, content);
|
|
279
276
|
}
|
|
280
|
-
function writeRemoteSchemaModule({ url, headers },
|
|
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,
|
|
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
|
-
`
|
|
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/
|
|
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
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
408
|
-
|
|
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,
|
|
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
|