@lewebsimple/nuxt-graphql 0.1.9 → 0.1.11

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.
Files changed (30) hide show
  1. package/dist/module.d.mts +7 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +83 -30
  4. package/dist/runtime/composables/useGraphQLCache.d.ts +5 -0
  5. package/dist/runtime/composables/useGraphQLCache.js +15 -0
  6. package/dist/runtime/composables/useGraphQLMutation.d.ts +1 -1
  7. package/dist/runtime/composables/useGraphQLMutation.js +7 -6
  8. package/dist/runtime/composables/useGraphQLQuery.d.ts +6 -1
  9. package/dist/runtime/composables/useGraphQLQuery.js +33 -7
  10. package/dist/runtime/composables/useGraphQLSubscription.d.ts +11 -0
  11. package/dist/runtime/composables/useGraphQLSubscription.js +46 -0
  12. package/dist/runtime/plugins/graphql.d.ts +5 -2
  13. package/dist/runtime/plugins/graphql.js +34 -5
  14. package/dist/runtime/server/utils/graphql-client.d.ts +15 -0
  15. package/dist/runtime/server/utils/graphql-client.js +14 -0
  16. package/dist/runtime/server/utils/useGraphQLMutation.d.ts +9 -0
  17. package/dist/runtime/server/utils/useGraphQLMutation.js +16 -0
  18. package/dist/runtime/server/utils/useGraphQLQuery.d.ts +4 -0
  19. package/dist/runtime/server/utils/useGraphQLQuery.js +7 -0
  20. package/dist/runtime/types/graphql-client.d.ts +10 -1
  21. package/dist/runtime/utils/graphql-cache.d.ts +26 -0
  22. package/dist/runtime/utils/graphql-cache.js +65 -0
  23. package/dist/runtime/utils/graphql-error.d.ts +13 -0
  24. package/dist/runtime/utils/graphql-error.js +24 -0
  25. package/dist/templates/yoga-handler.mjs +1 -0
  26. package/package.json +16 -12
  27. package/dist/runtime/composables/useGraphQL.d.ts +0 -4
  28. package/dist/runtime/composables/useGraphQL.js +0 -8
  29. /package/dist/runtime/server/{default-context.d.ts → graphql/default-context.d.ts} +0 -0
  30. /package/dist/runtime/server/{default-context.js → graphql/default-context.js} +0 -0
package/dist/module.d.mts CHANGED
@@ -1,10 +1,17 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
+ import { GraphQLCacheConfig } from '../dist/runtime/utils/graphql-cache.js';
2
3
 
3
4
  interface ModuleOptions {
4
5
  endpoint?: string;
6
+ headers?: Record<string, string>;
7
+ cache?: Partial<GraphQLCacheConfig>;
5
8
  codegen?: {
6
9
  pattern?: string;
7
10
  schemaOutput?: string;
11
+ scalars?: Record<string, string | {
12
+ input: string;
13
+ output: string;
14
+ }>;
8
15
  };
9
16
  }
10
17
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
3
  "configKey": "graphql",
4
- "version": "0.1.9",
4
+ "version": "0.1.11",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { useLogger, defineNuxtModule, createResolver, getLayerDirectories, addServerTemplate, addServerHandler, addImportsDir, addPlugin, addTypeTemplate } from '@nuxt/kit';
1
+ import { useLogger, defineNuxtModule, createResolver, getLayerDirectories, addServerTemplate, addServerHandler, addImportsDir, addServerImportsDir, addPlugin, addTypeTemplate } from '@nuxt/kit';
2
2
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { join, dirname, relative } from 'node:path';
4
4
  import { glob } from 'tinyglobby';
@@ -175,36 +175,65 @@ function formatDefinitions(defs) {
175
175
  return defs.map((def) => `${colorOf(def)}${def.name}${reset}`).join(`${dim} / ${reset}`);
176
176
  }
177
177
  async function runCodegen(options) {
178
- const { sdl, documents, operationsFile } = options;
178
+ const { sdl, documents, operationsFile, schemasFile, scalars } = options;
179
179
  if (documents.length === 0) {
180
180
  logger.warn("No GraphQL documents found");
181
181
  return;
182
182
  }
183
+ const zodScalars = {};
184
+ if (scalars) {
185
+ for (const [name, config] of Object.entries(scalars)) {
186
+ const inputType = typeof config === "string" ? config : config.input;
187
+ switch (inputType) {
188
+ case "Date":
189
+ zodScalars[name] = "z.coerce.date()";
190
+ break;
191
+ case "number":
192
+ zodScalars[name] = "z.coerce.number()";
193
+ break;
194
+ case "boolean":
195
+ zodScalars[name] = "z.coerce.boolean()";
196
+ break;
197
+ default:
198
+ zodScalars[name] = "z.string()";
199
+ }
200
+ }
201
+ }
183
202
  try {
184
- await generate(
185
- {
186
- schema: sdl,
187
- documents,
188
- generates: {
189
- [operationsFile]: {
190
- plugins: ["typescript", "typescript-operations", "typed-document-node"],
191
- config: {
192
- useTypeImports: true,
193
- enumsAsTypes: true,
194
- skipTypename: true,
195
- documentVariableSuffix: "Document",
196
- documentMode: "documentNode",
197
- strictScalars: true,
198
- defaultScalarType: "never"
199
- // TODO: Make codegen config customizable, e.g. for custom scalars
203
+ const generates = {
204
+ [operationsFile]: {
205
+ plugins: ["typescript", "typescript-operations", "typed-document-node"],
206
+ config: {
207
+ useTypeImports: true,
208
+ enumsAsTypes: true,
209
+ skipTypename: true,
210
+ documentVariableSuffix: "Document",
211
+ documentMode: "documentNode",
212
+ strictScalars: true,
213
+ defaultScalarType: "never",
214
+ scalars
215
+ }
216
+ }
217
+ };
218
+ if (schemasFile) {
219
+ generates[schemasFile] = {
220
+ plugins: ["typescript-validation-schema"],
221
+ config: {
222
+ schema: "zodv4",
223
+ importFrom: "#graphql/operations",
224
+ useTypeImports: true,
225
+ directives: {
226
+ constraint: {
227
+ minLength: "min",
228
+ maxLength: "max",
229
+ pattern: "regex"
200
230
  }
201
- }
202
- },
203
- silent: true,
204
- errorsOnly: true
205
- },
206
- true
207
- );
231
+ },
232
+ scalarSchemas: zodScalars
233
+ }
234
+ };
235
+ }
236
+ await generate({ schema: sdl, documents, generates, silent: true, errorsOnly: true }, true);
208
237
  logger.success(`Generated types for ${documents.length} document(s)`);
209
238
  } catch (error) {
210
239
  logger.error("GraphQL codegen failed:", error instanceof Error ? error.message : error);
@@ -238,7 +267,7 @@ const module$1 = defineNuxtModule({
238
267
  const layerServerDirs = layerDirs.map(({ server }) => server);
239
268
  const layerRootDirs = layerDirs.map(({ root }) => root);
240
269
  const schemaPath = await findSingleFile(layerServerDirs, "graphql/schema.{ts,mjs}", true);
241
- const contextPath = await findSingleFile(layerServerDirs, "graphql/context.{ts,mjs}") || resolve("./runtime/server/default-context.ts");
270
+ const contextPath = await findSingleFile(layerServerDirs, "graphql/context.{ts,mjs}") || resolve("./runtime/server/graphql/default-context.ts");
242
271
  nuxt.hook("nitro:config", (config) => {
243
272
  config.alias ||= {};
244
273
  config.alias["#graphql/schema"] = schemaPath;
@@ -253,13 +282,23 @@ const module$1 = defineNuxtModule({
253
282
  nuxt.hook("listen", (_, { url }) => {
254
283
  logger.success(`GraphQL Yoga ready at ${cyan}${url.replace(/\/$/, "")}${endpoint}${reset}`);
255
284
  });
256
- nuxt.options.runtimeConfig.public.graphql = { endpoint };
285
+ nuxt.options.runtimeConfig.public.graphql = {
286
+ endpoint,
287
+ headers: options.headers || {},
288
+ cache: {
289
+ enabled: options.cache?.enabled ?? false,
290
+ ttl: options.cache?.ttl ?? 6e4,
291
+ storage: options.cache?.storage ?? "memory"
292
+ }
293
+ };
257
294
  const codegenPattern = options.codegen?.pattern ?? "**/*.gql";
258
295
  const graphqlrcFile = join(rootDir, ".graphqlrc");
259
296
  const operationsFile = join(nuxt.options.buildDir, "graphql/operations.ts");
260
297
  const registryFile = join(nuxt.options.buildDir, "graphql/registry.ts");
298
+ const schemasFile = join(nuxt.options.buildDir, "graphql/schemas.ts");
261
299
  nuxt.options.alias["#graphql/operations"] = operationsFile;
262
300
  nuxt.options.alias["#graphql/registry"] = registryFile;
301
+ nuxt.options.alias["#graphql/schemas"] = schemasFile;
263
302
  const schemaOutput = options.codegen?.schemaOutput ?? "server/graphql/schema.graphql";
264
303
  if (schemaOutput) {
265
304
  if (!schemaOutput.endsWith(".graphql")) {
@@ -279,12 +318,24 @@ const module$1 = defineNuxtModule({
279
318
  const defs = analysis.byFile.get(doc.path) ?? [];
280
319
  logger.info(`${cyan}${relativePath}${reset} [${formatDefinitions(defs)}]`);
281
320
  }
282
- await runCodegen({ sdl, documents, operationsFile });
321
+ await runCodegen({
322
+ sdl,
323
+ documents,
324
+ operationsFile,
325
+ schemasFile,
326
+ scalars: options.codegen?.scalars
327
+ });
283
328
  if (writeFileIfChanged(schemaFile, sdl)) {
284
329
  logger.info(`GraphQL schema saved to ${cyan}${schemaOutput}${reset}`);
285
330
  }
286
- const config = JSON.stringify({ schema: relative(rootDir, schemaFile), documents: codegenPattern }, null, 2);
287
- if (writeFileIfChanged(graphqlrcFile, config)) {
331
+ const graphqlrc = {
332
+ schema: relative(rootDir, schemaFile),
333
+ documents: codegenPattern
334
+ };
335
+ if (options.codegen?.scalars) {
336
+ graphqlrc.scalars = options.codegen.scalars;
337
+ }
338
+ if (writeFileIfChanged(graphqlrcFile, JSON.stringify(graphqlrc, null, 2))) {
288
339
  logger.info(`GraphQL config saved to ${cyan}.graphqlrc${reset}`);
289
340
  }
290
341
  if (writeFileIfChanged(registryFile, generateRegistryByTypeSource(analysis.operationsByType))) {
@@ -295,6 +346,7 @@ const module$1 = defineNuxtModule({
295
346
  await generate();
296
347
  if (existsSync(operationsFile)) references.push({ path: operationsFile });
297
348
  if (existsSync(registryFile)) references.push({ path: registryFile });
349
+ if (existsSync(schemasFile)) references.push({ path: schemasFile });
298
350
  });
299
351
  if (nuxt.options.dev) {
300
352
  nuxt.hook("builder:watch", async (event, path) => {
@@ -304,6 +356,7 @@ const module$1 = defineNuxtModule({
304
356
  });
305
357
  }
306
358
  addImportsDir(resolve("./runtime/composables"));
359
+ addServerImportsDir(resolve("./runtime/server/utils"));
307
360
  addPlugin(resolve("./runtime/plugins/graphql"));
308
361
  addTypeTemplate({
309
362
  filename: "types/graphql-client.d.ts",
@@ -0,0 +1,5 @@
1
+ import type { QueryName, QueryVariables } from "#graphql/registry";
2
+ export declare function useGraphQLCache(): {
3
+ enabled: any;
4
+ invalidate: <N extends QueryName>(operationName?: N, variables?: QueryVariables<N>) => Promise<void>;
5
+ };
@@ -0,0 +1,15 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { cacheInvalidate, initCache } from "../utils/graphql-cache.js";
3
+ export function useGraphQLCache() {
4
+ const { public: { graphql: { cache: cacheConfig } } } = useRuntimeConfig();
5
+ if (import.meta.client && cacheConfig.enabled) {
6
+ initCache(cacheConfig.storage);
7
+ }
8
+ async function invalidate(operationName, variables) {
9
+ await cacheInvalidate(operationName, variables);
10
+ }
11
+ return {
12
+ enabled: cacheConfig.enabled,
13
+ invalidate
14
+ };
15
+ }
@@ -1,7 +1,7 @@
1
1
  import { type MutationName, type MutationResult, type MutationVariables } from "#graphql/registry";
2
2
  import type { IsEmptyObject } from "../utils/helpers.js";
3
3
  export declare function useGraphQLMutation<N extends MutationName>(operationName: N): {
4
- mutate: (...args: IsEmptyObject<MutationVariables<N>> extends true ? [variables?: MutationVariables<N>] : [variables: MutationVariables<N>]) => Promise<{
4
+ mutate: (...args: IsEmptyObject<MutationVariables<N>> extends true ? [variables?: MutationVariables<N>, headers?: HeadersInit] : [variables: MutationVariables<N>, headers?: HeadersInit]) => Promise<{
5
5
  data: MutationResult<N> | null;
6
6
  error: Error | null;
7
7
  }>;
@@ -1,18 +1,19 @@
1
1
  import { ref } from "vue";
2
- import { useGraphQL } from "./useGraphQL.js";
2
+ import { useNuxtApp } from "#imports";
3
3
  import { mutations } from "#graphql/registry";
4
+ import { wrapError } from "../utils/graphql-error.js";
4
5
  export function useGraphQLMutation(operationName) {
5
6
  const document = mutations[operationName];
6
- const { request } = useGraphQL();
7
+ const { $graphql } = useNuxtApp();
7
8
  const pending = ref(false);
8
9
  async function mutate(...args) {
9
10
  pending.value = true;
10
11
  try {
11
- const result = await request(document, args[0]);
12
+ const [variables, headers] = args;
13
+ const result = await $graphql().request(document, variables, headers);
12
14
  return { data: result, error: null };
13
- } catch (e) {
14
- const error = e instanceof Error ? e : new Error(String(e));
15
- return { data: null, error };
15
+ } catch (error) {
16
+ return { data: null, error: wrapError(error) };
16
17
  } finally {
17
18
  pending.value = false;
18
19
  }
@@ -1,4 +1,9 @@
1
1
  import type { AsyncData, AsyncDataOptions } from "#app";
2
2
  import { type QueryName, type QueryResult, type QueryVariables } from "#graphql/registry";
3
+ import { type CacheOptions } from "../utils/graphql-cache.js";
3
4
  import type { IsEmptyObject } from "../utils/helpers.js";
4
- export declare function useGraphQLQuery<N extends QueryName>(operationName: N, ...args: IsEmptyObject<QueryVariables<N>> extends true ? [variables?: QueryVariables<N>, opts?: AsyncDataOptions<QueryResult<N>>] : [variables: QueryVariables<N>, opts?: AsyncDataOptions<QueryResult<N>>]): AsyncData<QueryResult<N>, Error | null>;
5
+ export interface UseGraphQLQueryOptions<T> extends AsyncDataOptions<T> {
6
+ cache?: CacheOptions | false;
7
+ headers?: HeadersInit;
8
+ }
9
+ export declare function useGraphQLQuery<N extends QueryName>(operationName: N, ...args: IsEmptyObject<QueryVariables<N>> extends true ? [variables?: QueryVariables<N>, options?: UseGraphQLQueryOptions<QueryResult<N>>] : [variables: QueryVariables<N>, options?: UseGraphQLQueryOptions<QueryResult<N>>]): AsyncData<QueryResult<N>, Error | null>;
@@ -1,11 +1,37 @@
1
- import { useAsyncData } from "#imports";
2
- import { hash } from "ohash";
3
- import { useGraphQL } from "./useGraphQL.js";
1
+ import { useAsyncData, useNuxtApp, useRuntimeConfig, onScopeDispose } from "#imports";
4
2
  import { queries } from "#graphql/registry";
3
+ import { cacheGet, cacheSet, dedupeGet, dedupeSet, registerRefresh, initCache, getCacheKey } from "../utils/graphql-cache.js";
5
4
  export function useGraphQLQuery(operationName, ...args) {
5
+ const { $graphql } = useNuxtApp();
6
+ const { public: { graphql: { cache: globalCache } } } = useRuntimeConfig();
6
7
  const document = queries[operationName];
7
- const [variables, opts] = args;
8
- const { request } = useGraphQL();
9
- const key = `graphql:query:${operationName}:${hash(variables ?? {})}`;
10
- return useAsyncData(key, () => request(document, variables), opts);
8
+ const [variables, options] = args;
9
+ const cacheEnabled = options?.cache !== false && globalCache.enabled;
10
+ const cacheTtl = options?.cache === false ? 0 : options?.cache?.ttl ?? globalCache.ttl;
11
+ if (import.meta.client && cacheEnabled) {
12
+ initCache(globalCache.storage);
13
+ }
14
+ const key = getCacheKey(operationName, variables);
15
+ const fetcher = async () => {
16
+ if (import.meta.client && cacheEnabled) {
17
+ const cached = await cacheGet(operationName, variables);
18
+ if (cached) return cached;
19
+ }
20
+ const inFlight = dedupeGet(operationName, variables);
21
+ if (inFlight) return inFlight;
22
+ const promise = $graphql().request(document, variables, options?.headers);
23
+ dedupeSet(operationName, variables, promise);
24
+ const result = await promise;
25
+ if (import.meta.client && cacheEnabled && cacheTtl) {
26
+ await cacheSet(operationName, variables, result, cacheTtl);
27
+ }
28
+ return result;
29
+ };
30
+ const { cache: _cache, headers: _headers, ...asyncDataOptions } = options ?? {};
31
+ const asyncData = useAsyncData(key, fetcher, asyncDataOptions);
32
+ if (import.meta.client) {
33
+ const unregister = registerRefresh(operationName, variables, () => asyncData.refresh());
34
+ onScopeDispose(unregister);
35
+ }
36
+ return asyncData;
11
37
  }
@@ -0,0 +1,11 @@
1
+ import { type MaybeRefOrGetter, type Ref } from "vue";
2
+ import { type SubscriptionName, type SubscriptionResult, type SubscriptionVariables } from "#graphql/registry";
3
+ import { type GraphQLClientError } from "../utils/graphql-error.js";
4
+ import type { IsEmptyObject } from "../utils/helpers.js";
5
+ export type UseGraphQLSubscriptionReturn<N extends SubscriptionName> = {
6
+ data: Ref<SubscriptionResult<N> | null>;
7
+ error: Ref<GraphQLClientError | null>;
8
+ start: () => void;
9
+ stop: () => void;
10
+ };
11
+ export declare function useGraphQLSubscription<N extends SubscriptionName>(operationName: N, ...args: IsEmptyObject<SubscriptionVariables<N>> extends true ? [variables?: MaybeRefOrGetter<SubscriptionVariables<N>>] : [variables: MaybeRefOrGetter<SubscriptionVariables<N>>]): UseGraphQLSubscriptionReturn<N>;
@@ -0,0 +1,46 @@
1
+ import { ref, onScopeDispose, toValue } from "vue";
2
+ import { print } from "graphql";
3
+ import { useNuxtApp } from "#imports";
4
+ import { subscriptions } from "#graphql/registry";
5
+ import { wrapError } from "../utils/graphql-error.js";
6
+ export function useGraphQLSubscription(operationName, ...args) {
7
+ const { $graphqlSSE } = useNuxtApp();
8
+ const [variables] = args;
9
+ const data = ref(null);
10
+ const error = ref(null);
11
+ let unsubscribe = null;
12
+ function start() {
13
+ stop();
14
+ error.value = null;
15
+ unsubscribe = $graphqlSSE().subscribe(
16
+ {
17
+ query: print(subscriptions[operationName]),
18
+ variables: toValue(variables)
19
+ },
20
+ {
21
+ next: (result) => {
22
+ if (result.errors?.length) {
23
+ error.value = wrapError({ errors: result.errors });
24
+ } else if (result.data) {
25
+ data.value = result.data;
26
+ }
27
+ },
28
+ error: (e) => {
29
+ error.value = wrapError(e);
30
+ },
31
+ complete: () => {
32
+ unsubscribe = null;
33
+ }
34
+ }
35
+ );
36
+ }
37
+ function stop() {
38
+ unsubscribe?.();
39
+ unsubscribe = null;
40
+ }
41
+ if (import.meta.client) {
42
+ start();
43
+ }
44
+ onScopeDispose(stop);
45
+ return { data, error, start, stop };
46
+ }
@@ -1,7 +1,10 @@
1
1
  import { GraphQLClient } from "graphql-request";
2
+ import { type Client as SSEClient } from "graphql-sse";
2
3
  declare const _default: import("nuxt/app").Plugin<{
3
- graphql: GraphQLClient;
4
+ graphql: () => GraphQLClient;
5
+ graphqlSSE: () => SSEClient;
4
6
  }> & import("nuxt/app").ObjectPlugin<{
5
- graphql: GraphQLClient;
7
+ graphql: () => GraphQLClient;
8
+ graphqlSSE: () => SSEClient;
6
9
  }>;
7
10
  export default _default;
@@ -1,12 +1,41 @@
1
1
  import { GraphQLClient } from "graphql-request";
2
- import { defineNuxtPlugin, useRequestURL, useRuntimeConfig } from "#imports";
3
- export default defineNuxtPlugin(() => {
4
- const config = useRuntimeConfig();
2
+ import { createClient } from "graphql-sse";
3
+ import { defineNuxtPlugin, useRequestHeaders, useRequestURL, useRuntimeConfig } from "#imports";
4
+ import { wrapError } from "../utils/graphql-error.js";
5
+ export default defineNuxtPlugin((nuxtApp) => {
6
+ const { public: { graphql: { endpoint, headers: staticHeaders } } } = useRuntimeConfig();
5
7
  const { origin } = useRequestURL();
6
- const client = new GraphQLClient(`${origin}${config.public.graphql.endpoint}`);
8
+ const url = `${origin}${endpoint}`;
9
+ const getClient = () => {
10
+ const headers = { ...staticHeaders };
11
+ if (import.meta.server) {
12
+ const ssrHeaders = useRequestHeaders(["cookie", "authorization"]);
13
+ Object.assign(headers, ssrHeaders);
14
+ }
15
+ const client = new GraphQLClient(url, {
16
+ headers,
17
+ responseMiddleware: (response) => {
18
+ if (response instanceof Error) {
19
+ nuxtApp.callHook("graphql:error", wrapError(response));
20
+ }
21
+ }
22
+ });
23
+ return client;
24
+ };
25
+ let sseClient = null;
26
+ const getSSEClient = () => {
27
+ if (import.meta.server) {
28
+ throw new Error("SSE subscriptions are not available on the server");
29
+ }
30
+ if (!sseClient) {
31
+ sseClient = createClient({ url, headers: staticHeaders });
32
+ }
33
+ return sseClient;
34
+ };
7
35
  return {
8
36
  provide: {
9
- graphql: client
37
+ graphql: getClient,
38
+ graphqlSSE: getSSEClient
10
39
  }
11
40
  };
12
41
  });
@@ -0,0 +1,15 @@
1
+ import type { H3Event } from "h3";
2
+ import { GraphQLClient } from "graphql-request";
3
+ /**
4
+ * Create server-side GraphQL client instance for the given H3 event.
5
+ *
6
+ * @param event H3 event
7
+ *
8
+ * @returns GraphQL client instance
9
+ */
10
+ export declare function getGraphQLClient(event: H3Event): GraphQLClient;
11
+ declare module "h3" {
12
+ interface H3EventContext {
13
+ __nuxtGraphQLClient?: GraphQLClient;
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ import { getRequestHeaders, getRequestURL } from "h3";
2
+ import { GraphQLClient } from "graphql-request";
3
+ import { useRuntimeConfig } from "#imports";
4
+ export function getGraphQLClient(event) {
5
+ if (event.context.__nuxtGraphQLClient) {
6
+ return event.context.__nuxtGraphQLClient;
7
+ }
8
+ const { public: { graphql: { endpoint } } } = useRuntimeConfig();
9
+ const { origin } = getRequestURL(event);
10
+ const url = `${origin}${endpoint}`;
11
+ const headers = getRequestHeaders(event);
12
+ event.context.__nuxtGraphQLClient = new GraphQLClient(url, { headers });
13
+ return event.context.__nuxtGraphQLClient;
14
+ }
@@ -0,0 +1,9 @@
1
+ import type { H3Event } from "h3";
2
+ import { type MutationName, type MutationResult, type MutationVariables } from "#graphql/registry";
3
+ import type { IsEmptyObject } from "../../utils/helpers.js";
4
+ export declare function useGraphQLMutation<N extends MutationName>(event: H3Event, operationName: N): Promise<{
5
+ mutate: (...args: IsEmptyObject<MutationVariables<N>> extends true ? [variables?: MutationVariables<N>, headers?: HeadersInit] : [variables: MutationVariables<N>, headers?: HeadersInit]) => Promise<{
6
+ data: MutationResult<N> | null;
7
+ error: Error | null;
8
+ }>;
9
+ }>;
@@ -0,0 +1,16 @@
1
+ import { getGraphQLClient } from "./graphql-client.js";
2
+ import { mutations } from "#graphql/registry";
3
+ export async function useGraphQLMutation(event, operationName) {
4
+ const client = getGraphQLClient(event);
5
+ async function mutate(...args) {
6
+ try {
7
+ const [variables, headers] = args;
8
+ const result = await client.request(mutations[operationName], variables, headers);
9
+ return { data: result, error: null };
10
+ } catch (e) {
11
+ const error = e instanceof Error ? e : new Error(String(e));
12
+ return { data: null, error };
13
+ }
14
+ }
15
+ return { mutate };
16
+ }
@@ -0,0 +1,4 @@
1
+ import type { H3Event } from "h3";
2
+ import { type QueryName, type QueryResult, type QueryVariables } from "#graphql/registry";
3
+ import type { IsEmptyObject } from "../../utils/helpers.js";
4
+ export declare function useGraphQLQuery<N extends QueryName>(event: H3Event, operationName: N, ...args: IsEmptyObject<QueryVariables<N>> extends true ? [variables?: QueryVariables<N>, headers?: HeadersInit] : [variables: QueryVariables<N>, headers?: HeadersInit]): Promise<QueryResult<N>>;
@@ -0,0 +1,7 @@
1
+ import { getGraphQLClient } from "./graphql-client.js";
2
+ import { queries } from "#graphql/registry";
3
+ export async function useGraphQLQuery(event, operationName, ...args) {
4
+ const client = getGraphQLClient(event);
5
+ const [variables, headers] = args;
6
+ return client.request(queries[operationName], variables, headers);
7
+ }
@@ -1,8 +1,15 @@
1
1
  import type { GraphQLClient } from "graphql-request";
2
+ import type { Client as SSEClient } from "graphql-sse";
3
+ import type { GraphQLClientError } from "../utils/graphql-error";
2
4
 
3
5
  declare module "#app" {
4
6
  interface NuxtApp {
5
- $graphql: GraphQLClient;
7
+ $graphql: () => GraphQLClient;
8
+ $graphqlSSE: () => SSEClient;
9
+ }
10
+ interface RuntimeNuxtHooks {
11
+ "graphql:headers": (headers: Record<string, string>) => void | Promise<void>;
12
+ "graphql:error": (error: GraphQLClientError) => void;
6
13
  }
7
14
  }
8
15
 
@@ -10,6 +17,8 @@ declare module "nuxt/schema" {
10
17
  interface PublicRuntimeConfig {
11
18
  graphql: {
12
19
  endpoint: string;
20
+ headers: Record<string, string>;
21
+ cache: GraphQLCacheConfig;
13
22
  };
14
23
  }
15
24
  }
@@ -0,0 +1,26 @@
1
+ export interface GraphQLCacheConfig {
2
+ enabled: boolean;
3
+ ttl: number;
4
+ storage: "memory" | "localStorage";
5
+ }
6
+ export interface CacheOptions {
7
+ enabled?: boolean;
8
+ ttl?: number;
9
+ }
10
+ export declare function initCache(type: "memory" | "localStorage"): void;
11
+ export declare function getCacheKey(operationName: string, variables: unknown): string;
12
+ export declare function cacheGet<T>(operationName: string, variables: unknown): Promise<T | null>;
13
+ export declare function cacheSet<T>(operationName: string, variables: unknown, data: T, ttl: number): Promise<void>;
14
+ export declare function dedupeGet(operationName: string, variables: unknown): Promise<unknown> | null;
15
+ export declare function dedupeSet(operationName: string, variables: unknown, promise: Promise<unknown>): void;
16
+ /**
17
+ * Register a refresh callback for a query
18
+ */
19
+ export declare function registerRefresh(operationName: string, variables: unknown, refresh: () => void): () => void;
20
+ /**
21
+ * Invalidate cached queries and trigger refreshes
22
+ * - No args: invalidate all
23
+ * - operationName only: invalidate all queries with that name
24
+ * - operationName + variables: invalidate specific query
25
+ */
26
+ export declare function cacheInvalidate(operationName?: string, variables?: unknown): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import { createStorage } from "unstorage";
2
+ import memoryDriver from "unstorage/drivers/memory";
3
+ import localStorageDriver from "unstorage/drivers/localstorage";
4
+ import { hash } from "ohash";
5
+ const inFlight = /* @__PURE__ */ new Map();
6
+ const refreshCallbacks = /* @__PURE__ */ new Map();
7
+ let storage = null;
8
+ export function initCache(type) {
9
+ if (storage) return;
10
+ storage = createStorage({
11
+ driver: type === "localStorage" ? localStorageDriver({ base: "graphql:" }) : memoryDriver()
12
+ });
13
+ }
14
+ export function getCacheKey(operationName, variables) {
15
+ return `${operationName}:${hash(variables ?? {})}`;
16
+ }
17
+ export async function cacheGet(operationName, variables) {
18
+ if (!storage) return null;
19
+ const key = getCacheKey(operationName, variables);
20
+ const entry = await storage.getItem(key);
21
+ if (!entry) return null;
22
+ if (Date.now() > entry.expires) {
23
+ await storage.removeItem(key);
24
+ return null;
25
+ }
26
+ return entry.data;
27
+ }
28
+ export async function cacheSet(operationName, variables, data, ttl) {
29
+ if (!storage) return;
30
+ const key = getCacheKey(operationName, variables);
31
+ const entry = { data, expires: Date.now() + ttl };
32
+ await storage.setItem(key, entry);
33
+ }
34
+ export function dedupeGet(operationName, variables) {
35
+ const key = getCacheKey(operationName, variables);
36
+ return inFlight.get(key) ?? null;
37
+ }
38
+ export function dedupeSet(operationName, variables, promise) {
39
+ const key = getCacheKey(operationName, variables);
40
+ inFlight.set(key, promise);
41
+ promise.finally(() => inFlight.delete(key));
42
+ }
43
+ export function registerRefresh(operationName, variables, refresh) {
44
+ const key = getCacheKey(operationName, variables);
45
+ if (!refreshCallbacks.has(key)) {
46
+ refreshCallbacks.set(key, /* @__PURE__ */ new Set());
47
+ }
48
+ refreshCallbacks.get(key).add(refresh);
49
+ return () => refreshCallbacks.get(key)?.delete(refresh);
50
+ }
51
+ export async function cacheInvalidate(operationName, variables) {
52
+ if (!storage) return;
53
+ if (operationName && variables !== void 0) {
54
+ const key = getCacheKey(operationName, variables);
55
+ await storage.removeItem(key);
56
+ refreshCallbacks.get(key)?.forEach((cb) => cb());
57
+ } else {
58
+ const keys = await storage.getKeys();
59
+ const toRemove = operationName ? keys.filter((k) => k.startsWith(`${operationName}:`)) : keys;
60
+ await Promise.all(toRemove.map((k) => storage.removeItem(k)));
61
+ for (const key of toRemove) {
62
+ refreshCallbacks.get(key)?.forEach((cb) => cb());
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,13 @@
1
+ import type { GraphQLError } from "graphql";
2
+ export declare class GraphQLClientError extends Error {
3
+ readonly errors: GraphQLError[];
4
+ constructor(message: string, errors?: GraphQLError[]);
5
+ }
6
+ /**
7
+ * Wrap a generic error into a GraphQLClientError
8
+ *
9
+ * @param error Generic error
10
+ *
11
+ * @returns Wrapped GraphQLClientError
12
+ */
13
+ export declare function wrapError(error: unknown): GraphQLClientError;
@@ -0,0 +1,24 @@
1
+ export class GraphQLClientError extends Error {
2
+ errors;
3
+ constructor(message, errors = []) {
4
+ super(message);
5
+ this.name = "GraphQLClientError";
6
+ this.errors = errors;
7
+ }
8
+ }
9
+ export function wrapError(error) {
10
+ if (error instanceof GraphQLClientError) {
11
+ return error;
12
+ }
13
+ if (error && typeof error === "object" && "response" in error) {
14
+ const clientError = error;
15
+ return new GraphQLClientError(clientError.message, clientError.response?.errors);
16
+ }
17
+ if (error && typeof error === "object" && "errors" in error) {
18
+ const { errors } = error;
19
+ const message2 = errors.map((e) => e.message).join(", ");
20
+ return new GraphQLClientError(message2, errors);
21
+ }
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ return new GraphQLClientError(message);
24
+ }
@@ -13,6 +13,7 @@ function getYoga() {
13
13
  graphqlEndpoint: "{{endpoint}}",
14
14
  fetchAPI: globalThis,
15
15
  graphiql: process.env.NODE_ENV !== "production",
16
+ subscriptions: { protocol: "SSE" },
16
17
  });
17
18
  }
18
19
  return yoga;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Opinionated Nuxt module for using GraphQL",
5
5
  "repository": "lewebsimple/nuxt-graphql",
6
6
  "license": "MIT",
@@ -22,6 +22,17 @@
22
22
  "files": [
23
23
  "dist"
24
24
  ],
25
+ "scripts": {
26
+ "prepack": "nuxt-module-build build",
27
+ "dev": "npm run dev:prepare && nuxi dev playground",
28
+ "dev:build": "nuxi build playground",
29
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
30
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release --push && npm publish",
31
+ "lint": "eslint .",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest watch",
34
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
35
+ },
25
36
  "dependencies": {
26
37
  "@graphql-codegen/cli": "^5.0.7",
27
38
  "@graphql-codegen/typed-document-node": "^5.1.2",
@@ -30,11 +41,14 @@
30
41
  "@graphql-typed-document-node/core": "^3.2.0",
31
42
  "@nuxt/kit": "^4.2.2",
32
43
  "graphql": "^16.12.0",
44
+ "graphql-codegen-typescript-validation-schema": "^0.18.1",
33
45
  "graphql-request": "^7.4.0",
46
+ "graphql-sse": "^2.6.0",
34
47
  "graphql-yoga": "^5.18.0",
35
48
  "jiti": "^2.6.1",
36
49
  "ohash": "^2.0.11",
37
- "tinyglobby": "^0.2.15"
50
+ "tinyglobby": "^0.2.15",
51
+ "unstorage": "^1.17.3"
38
52
  },
39
53
  "devDependencies": {
40
54
  "@nuxt/devtools": "^3.1.1",
@@ -58,15 +72,5 @@
58
72
  "types": {
59
73
  "chore": false
60
74
  }
61
- },
62
- "scripts": {
63
- "dev": "npm run dev:prepare && nuxi dev playground",
64
- "dev:build": "nuxi build playground",
65
- "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
66
- "release": "npm run lint && npm run test && npm run prepack && changelogen --release --push && npm publish",
67
- "lint": "eslint .",
68
- "test": "vitest run",
69
- "test:watch": "vitest watch",
70
- "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
71
75
  }
72
76
  }
@@ -1,4 +0,0 @@
1
- import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
2
- export declare function useGraphQL(): {
3
- request: <TResult, TVariables extends Record<string, unknown> | undefined>(document: TypedDocumentNode<TResult, TVariables>, variables?: TVariables) => Promise<TResult>;
4
- };
@@ -1,8 +0,0 @@
1
- import { useNuxtApp } from "#imports";
2
- export function useGraphQL() {
3
- const { $graphql } = useNuxtApp();
4
- async function request(document, variables) {
5
- return $graphql.request({ document, variables });
6
- }
7
- return { request };
8
- }