@lewebsimple/nuxt-graphql 0.1.10 → 0.1.12

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/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.10",
4
+ "version": "0.1.12",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -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);
@@ -253,18 +282,26 @@ 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
- if (schemaOutput) {
265
- if (!schemaOutput.endsWith(".graphql")) {
266
- logger.warn(`Schema output '${schemaOutput}' should have .graphql extension.`);
267
- }
303
+ if (schemaOutput && !schemaOutput.endsWith(".graphql")) {
304
+ logger.warn(`Schema output '${schemaOutput}' should have .graphql extension.`);
268
305
  }
269
306
  const schemaFile = join(rootDir, schemaOutput);
270
307
  const generate = async () => {
@@ -279,12 +316,24 @@ const module$1 = defineNuxtModule({
279
316
  const defs = analysis.byFile.get(doc.path) ?? [];
280
317
  logger.info(`${cyan}${relativePath}${reset} [${formatDefinitions(defs)}]`);
281
318
  }
282
- await runCodegen({ sdl, documents, operationsFile });
319
+ await runCodegen({
320
+ sdl,
321
+ documents,
322
+ operationsFile,
323
+ schemasFile,
324
+ scalars: options.codegen?.scalars
325
+ });
283
326
  if (writeFileIfChanged(schemaFile, sdl)) {
284
327
  logger.info(`GraphQL schema saved to ${cyan}${schemaOutput}${reset}`);
285
328
  }
286
- const config = JSON.stringify({ schema: relative(rootDir, schemaFile), documents: codegenPattern }, null, 2);
287
- if (writeFileIfChanged(graphqlrcFile, config)) {
329
+ const graphqlrc = {
330
+ schema: relative(rootDir, schemaFile),
331
+ documents: codegenPattern
332
+ };
333
+ if (options.codegen?.scalars) {
334
+ graphqlrc.scalars = options.codegen.scalars;
335
+ }
336
+ if (writeFileIfChanged(graphqlrcFile, JSON.stringify(graphqlrc, null, 2))) {
288
337
  logger.info(`GraphQL config saved to ${cyan}.graphqlrc${reset}`);
289
338
  }
290
339
  if (writeFileIfChanged(registryFile, generateRegistryByTypeSource(analysis.operationsByType))) {
@@ -295,6 +344,7 @@ const module$1 = defineNuxtModule({
295
344
  await generate();
296
345
  if (existsSync(operationsFile)) references.push({ path: operationsFile });
297
346
  if (existsSync(registryFile)) references.push({ path: registryFile });
347
+ if (existsSync(schemasFile)) references.push({ path: schemasFile });
298
348
  });
299
349
  if (nuxt.options.dev) {
300
350
  nuxt.hook("builder:watch", async (event, path) => {
@@ -0,0 +1,10 @@
1
+ import type { QueryName, QueryVariables } from "#graphql/registry";
2
+ /**
3
+ * GraphQL cache management composable
4
+ *
5
+ * @returns Object with enabled flag and invalidate function
6
+ */
7
+ export declare function useGraphQLCache(): {
8
+ enabled: any;
9
+ invalidate: <N extends QueryName>(operationName?: N, variables?: QueryVariables<N>) => Promise<void>;
10
+ };
@@ -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,5 +1,11 @@
1
1
  import { type MutationName, type MutationResult, type MutationVariables } from "#graphql/registry";
2
2
  import type { IsEmptyObject } from "../utils/helpers.js";
3
+ /**
4
+ * Client-side GraphQL mutation composable
5
+ *
6
+ * @param operationName Mutation operation name
7
+ * @returns Object with mutate function and pending state
8
+ */
3
9
  export declare function useGraphQLMutation<N extends MutationName>(operationName: N): {
4
10
  mutate: (...args: IsEmptyObject<MutationVariables<N>> extends true ? [variables?: MutationVariables<N>, headers?: HeadersInit] : [variables: MutationVariables<N>, headers?: HeadersInit]) => Promise<{
5
11
  data: MutationResult<N> | null;
@@ -1,6 +1,7 @@
1
1
  import { ref } from "vue";
2
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
7
  const { $graphql } = useNuxtApp();
@@ -11,9 +12,8 @@ export function useGraphQLMutation(operationName) {
11
12
  const [variables, headers] = args;
12
13
  const result = await $graphql().request(document, variables, headers);
13
14
  return { data: result, error: null };
14
- } catch (e) {
15
- const error = e instanceof Error ? e : new Error(String(e));
16
- return { data: null, error };
15
+ } catch (error) {
16
+ return { data: null, error: wrapError(error) };
17
17
  } finally {
18
18
  pending.value = false;
19
19
  }
@@ -1,4 +1,16 @@
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>, options?: AsyncDataOptions<QueryResult<N>>, headers?: HeadersInit] : [variables: QueryVariables<N>, options?: AsyncDataOptions<QueryResult<N>>, headers?: HeadersInit]): AsyncData<QueryResult<N>, Error | null>;
5
+ export interface UseGraphQLQueryOptions<T> extends AsyncDataOptions<T> {
6
+ cache?: CacheOptions | false;
7
+ headers?: HeadersInit;
8
+ }
9
+ /**
10
+ * Client-side GraphQL query composable with caching and deduplication
11
+ *
12
+ * @param operationName Query operation name
13
+ * @param args Variables and optional configuration
14
+ * @returns AsyncData object with query result
15
+ */
16
+ 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,10 +1,37 @@
1
- import { useAsyncData, useNuxtApp } from "#imports";
2
- import { hash } from "ohash";
1
+ import { useAsyncData, useNuxtApp, useRuntimeConfig, onScopeDispose } from "#imports";
3
2
  import { queries } from "#graphql/registry";
3
+ import { cacheGet, cacheSet, dedupeGet, dedupeSet, registerRefresh, initCache, getCacheKey } from "../utils/graphql-cache.js";
4
4
  export function useGraphQLQuery(operationName, ...args) {
5
5
  const { $graphql } = useNuxtApp();
6
+ const { public: { graphql: { cache: globalCache } } } = useRuntimeConfig();
6
7
  const document = queries[operationName];
7
- const [variables, options, headers] = args;
8
- const key = `graphql:query:${operationName}:${hash(variables ?? {})}`;
9
- return useAsyncData(key, () => $graphql().request(document, variables, headers), options);
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;
10
37
  }
@@ -1,10 +1,18 @@
1
1
  import { type MaybeRefOrGetter, type Ref } from "vue";
2
2
  import { type SubscriptionName, type SubscriptionResult, type SubscriptionVariables } from "#graphql/registry";
3
+ import { type GraphQLClientError } from "../utils/graphql-error.js";
3
4
  import type { IsEmptyObject } from "../utils/helpers.js";
4
5
  export type UseGraphQLSubscriptionReturn<N extends SubscriptionName> = {
5
6
  data: Ref<SubscriptionResult<N> | null>;
6
- error: Ref<Error | null>;
7
+ error: Ref<GraphQLClientError | null>;
7
8
  start: () => void;
8
9
  stop: () => void;
9
10
  };
11
+ /**
12
+ * Client-side GraphQL subscription composable using SSE
13
+ *
14
+ * @param operationName Subscription operation name
15
+ * @param args Variables (can be reactive)
16
+ * @returns Object with data, error, start, and stop
17
+ */
10
18
  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>;
@@ -2,6 +2,7 @@ import { ref, onScopeDispose, toValue } from "vue";
2
2
  import { print } from "graphql";
3
3
  import { useNuxtApp } from "#imports";
4
4
  import { subscriptions } from "#graphql/registry";
5
+ import { wrapError } from "../utils/graphql-error.js";
5
6
  export function useGraphQLSubscription(operationName, ...args) {
6
7
  const { $graphqlSSE } = useNuxtApp();
7
8
  const [variables] = args;
@@ -19,13 +20,13 @@ export function useGraphQLSubscription(operationName, ...args) {
19
20
  {
20
21
  next: (result) => {
21
22
  if (result.errors?.length) {
22
- error.value = new Error(result.errors.map((e) => e.message).join(", "));
23
+ error.value = wrapError({ errors: result.errors });
23
24
  } else if (result.data) {
24
25
  data.value = result.data;
25
26
  }
26
27
  },
27
28
  error: (e) => {
28
- error.value = e instanceof Error ? e : new Error(String(e));
29
+ error.value = wrapError(e);
29
30
  },
30
31
  complete: () => {
31
32
  unsubscribe = null;
@@ -1,19 +1,25 @@
1
1
  import { GraphQLClient } from "graphql-request";
2
2
  import { createClient } from "graphql-sse";
3
3
  import { defineNuxtPlugin, useRequestHeaders, useRequestURL, useRuntimeConfig } from "#imports";
4
- export default defineNuxtPlugin(() => {
5
- const { public: { graphql: { endpoint } } } = useRuntimeConfig();
4
+ import { wrapError } from "../utils/graphql-error.js";
5
+ export default defineNuxtPlugin((nuxtApp) => {
6
+ const { public: { graphql: { endpoint, headers: staticHeaders } } } = useRuntimeConfig();
6
7
  const { origin } = useRequestURL();
7
8
  const url = `${origin}${endpoint}`;
8
- let client = null;
9
9
  const getClient = () => {
10
- if (!client) {
11
- client = new GraphQLClient(url);
12
- }
10
+ const headers = { ...staticHeaders };
13
11
  if (import.meta.server) {
14
- const headers = useRequestHeaders(["cookie", "authorization"]);
15
- client.setHeaders(headers);
12
+ const ssrHeaders = useRequestHeaders(["cookie", "authorization"]);
13
+ Object.assign(headers, ssrHeaders);
16
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
+ });
17
23
  return client;
18
24
  };
19
25
  let sseClient = null;
@@ -22,7 +28,7 @@ export default defineNuxtPlugin(() => {
22
28
  throw new Error("SSE subscriptions are not available on the server");
23
29
  }
24
30
  if (!sseClient) {
25
- sseClient = createClient({ url });
31
+ sseClient = createClient({ url, headers: staticHeaders });
26
32
  }
27
33
  return sseClient;
28
34
  };
@@ -1,10 +1,9 @@
1
1
  import type { H3Event } from "h3";
2
2
  import { GraphQLClient } from "graphql-request";
3
3
  /**
4
- * Create server-side GraphQL client instance for the given H3 event.
4
+ * Get or create a server-side GraphQL client for an H3 event
5
5
  *
6
6
  * @param event H3 event
7
- *
8
7
  * @returns GraphQL client instance
9
8
  */
10
9
  export declare function getGraphQLClient(event: H3Event): GraphQLClient;
@@ -1,7 +1,14 @@
1
1
  import type { H3Event } from "h3";
2
2
  import { type MutationName, type MutationResult, type MutationVariables } from "#graphql/registry";
3
3
  import type { IsEmptyObject } from "../../utils/helpers.js";
4
- export declare function useGraphQLMutation<N extends MutationName>(event: H3Event, operationName: N): Promise<{
4
+ /**
5
+ * Server-side GraphQL mutation composable
6
+ *
7
+ * @param event H3 event
8
+ * @param operationName Mutation operation name
9
+ * @returns Object with mutate function
10
+ */
11
+ export declare function useServerGraphQLMutation<N extends MutationName>(event: H3Event, operationName: N): Promise<{
5
12
  mutate: (...args: IsEmptyObject<MutationVariables<N>> extends true ? [variables?: MutationVariables<N>, headers?: HeadersInit] : [variables: MutationVariables<N>, headers?: HeadersInit]) => Promise<{
6
13
  data: MutationResult<N> | null;
7
14
  error: Error | null;
@@ -1,6 +1,6 @@
1
1
  import { getGraphQLClient } from "./graphql-client.js";
2
2
  import { mutations } from "#graphql/registry";
3
- export async function useGraphQLMutation(event, operationName) {
3
+ export async function useServerGraphQLMutation(event, operationName) {
4
4
  const client = getGraphQLClient(event);
5
5
  async function mutate(...args) {
6
6
  try {
@@ -0,0 +1,12 @@
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
+ /**
5
+ * Server-side GraphQL query composable
6
+ *
7
+ * @param event H3 event
8
+ * @param operationName Query operation name
9
+ * @param args Variables and optional headers
10
+ * @returns Query result
11
+ */
12
+ export declare function useServerGraphQLQuery<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>>;
@@ -1,6 +1,6 @@
1
1
  import { getGraphQLClient } from "./graphql-client.js";
2
2
  import { queries } from "#graphql/registry";
3
- export async function useGraphQLQuery(event, operationName, ...args) {
3
+ export async function useServerGraphQLQuery(event, operationName, ...args) {
4
4
  const client = getGraphQLClient(event);
5
5
  const [variables, headers] = args;
6
6
  return client.request(queries[operationName], variables, headers);
@@ -1,17 +1,26 @@
1
1
  import type { GraphQLClient } from "graphql-request";
2
2
  import type { Client as SSEClient } from "graphql-sse";
3
+ import type { GraphQLClientError } from "../utils/graphql-error";
3
4
 
5
+ // Extend NuxtApp with GraphQL clients
4
6
  declare module "#app" {
5
7
  interface NuxtApp {
6
8
  $graphql: () => GraphQLClient;
7
9
  $graphqlSSE: () => SSEClient;
8
10
  }
11
+
12
+ interface RuntimeNuxtHooks {
13
+ "graphql:error": (error: GraphQLClientError) => void;
14
+ }
9
15
  }
10
16
 
17
+ // Extend Nuxt runtime config with GraphQL options
11
18
  declare module "nuxt/schema" {
12
19
  interface PublicRuntimeConfig {
13
20
  graphql: {
14
21
  endpoint: string;
22
+ headers: Record<string, string>;
23
+ cache: GraphQLCacheConfig;
15
24
  };
16
25
  }
17
26
  }
@@ -0,0 +1,36 @@
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
+ * @param operationName GraphQL operation name
20
+ * @param variables Query variables
21
+ * @param refresh Callback to execute on cache invalidation
22
+ * @returns Unregister function
23
+ */
24
+ export declare function registerRefresh(operationName: string, variables: unknown, refresh: () => void): () => void;
25
+ /**
26
+ * Invalidate cached queries and trigger refresh callbacks
27
+ *
28
+ * @param operationName Optional operation name to filter invalidation
29
+ * @param variables Optional variables to target specific query
30
+ *
31
+ * Usage:
32
+ * - No args: invalidate all queries
33
+ * - operationName only: invalidate all queries with that name
34
+ * - operationName + variables: invalidate specific query
35
+ */
36
+ 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,12 @@
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 from various sources
10
+ * @returns Wrapped GraphQLClientError
11
+ */
12
+ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Opinionated Nuxt module for using GraphQL",
5
5
  "repository": "lewebsimple/nuxt-graphql",
6
6
  "license": "MIT",
@@ -31,22 +31,25 @@
31
31
  "lint": "eslint .",
32
32
  "test": "vitest run",
33
33
  "test:watch": "vitest watch",
34
+ "test:coverage": "vitest run --coverage",
34
35
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
35
36
  },
36
37
  "dependencies": {
37
- "@graphql-codegen/cli": "^5.0.7",
38
- "@graphql-codegen/typed-document-node": "^5.1.2",
39
- "@graphql-codegen/typescript": "^4.1.6",
40
- "@graphql-codegen/typescript-operations": "^4.6.1",
38
+ "@graphql-codegen/cli": "^6.1.0",
39
+ "@graphql-codegen/typed-document-node": "^6.1.5",
40
+ "@graphql-codegen/typescript": "^5.0.7",
41
+ "@graphql-codegen/typescript-operations": "^5.0.7",
41
42
  "@graphql-typed-document-node/core": "^3.2.0",
42
43
  "@nuxt/kit": "^4.2.2",
43
44
  "graphql": "^16.12.0",
45
+ "graphql-codegen-typescript-validation-schema": "^0.18.1",
44
46
  "graphql-request": "^7.4.0",
45
47
  "graphql-sse": "^2.6.0",
46
48
  "graphql-yoga": "^5.18.0",
47
49
  "jiti": "^2.6.1",
48
50
  "ohash": "^2.0.11",
49
- "tinyglobby": "^0.2.15"
51
+ "tinyglobby": "^0.2.15",
52
+ "unstorage": "^1.17.3"
50
53
  },
51
54
  "devDependencies": {
52
55
  "@nuxt/devtools": "^3.1.1",
@@ -56,6 +59,7 @@
56
59
  "@nuxt/schema": "^4.2.2",
57
60
  "@nuxt/test-utils": "^3.21.0",
58
61
  "@types/node": "latest",
62
+ "@vitest/coverage-v8": "^4.0.16",
59
63
  "changelogen": "^0.6.2",
60
64
  "eslint": "^9.39.2",
61
65
  "nuxt": "^4.2.2",
@@ -71,4 +75,4 @@
71
75
  "chore": false
72
76
  }
73
77
  }
74
- }
78
+ }
@@ -1,4 +0,0 @@
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>>;