@lewebsimple/nuxt-graphql 0.6.11 → 0.6.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/README.md CHANGED
@@ -287,21 +287,75 @@ const { data } = await useAsyncGraphQLQuery("HelloWorld", undefined, {
287
287
  });
288
288
  ```
289
289
 
290
- #### Manual invalidation
290
+ #### Cache manipulation
291
291
 
292
- On the client, `useGraphQLCache()` can invalidate in-memory and persisted entries:
292
+ On the client, `useGraphQLCache()` provides helpers to read, write, update, and invalidate cache entries:
293
293
 
294
294
  ```ts
295
- const { invalidate } = useGraphQLCache();
295
+ const cache = useGraphQLCache();
296
296
 
297
- // Invalidate a single entry (operation + variables)
298
- await invalidate({ operation: "HelloWorld", variables: {} });
297
+ // Read cached query (in-memory only)
298
+ const films = cache.read("AllFilms", {});
299
299
 
300
- // Invalidate all entries for an operation
301
- await invalidate({ operation: "HelloWorld" });
300
+ // Write cached query synchronously (in-memory only, useful for rollbacks)
301
+ cache.write("AllFilms", {}, newValue);
302
+ cache.write("AllFilms", {}, (current) => ({ ...current, films: [...current.films, newFilm] }));
302
303
 
303
- // Invalidate all entries
304
- await invalidate();
304
+ // Update cached query asynchronously (in-memory + persisted)
305
+ await cache.update("AllFilms", {}, newValue);
306
+ await cache.update("AllFilms", {}, (current) => ({ ...current, films: [...current.films, newFilm] }));
307
+
308
+ // Invalidate cache entries
309
+ await cache.invalidate("HelloWorld", {}); // Exact match (operation + variables)
310
+ await cache.invalidate("HelloWorld"); // All entries for operation
311
+ await cache.invalidate(); // All entries
312
+ ```
313
+
314
+ > **⚠️ Important:** Cache manipulation methods (`read`, `write`, `update`, `invalidate`) are incompatible with the `transform` option on `useAsyncGraphQLQuery`. If you need to use cache invalidation or manipulation, do not use the `transform` option. Instead, transform the data after retrieving it from the composable.
315
+
316
+ #### Optimistic updates
317
+
318
+ `useGraphQLMutation` supports optimistic updates via lifecycle hooks:
319
+
320
+ ```ts
321
+ const { mutate } = useGraphQLMutation("AddFilm", {
322
+ onMutate: async (variables) => {
323
+ const cache = useGraphQLCache();
324
+
325
+ // Snapshot current value for rollback
326
+ const snapshot = cache.read("AllFilms", {});
327
+
328
+ // Optimistically update cache
329
+ await cache.update("AllFilms", {}, (current) => ({
330
+ films: [...(current?.films ?? []), { id: 'temp', title: variables.title }]
331
+ }));
332
+
333
+ return { snapshot };
334
+ },
335
+
336
+ onError: (error, variables, context) => {
337
+ const cache = useGraphQLCache();
338
+ // Rollback on error (sync for instant UI update)
339
+ if (context?.snapshot) {
340
+ cache.write("AllFilms", {}, context.snapshot);
341
+ }
342
+ },
343
+
344
+ onSuccess: (data, variables, context) => {
345
+ // Replace optimistic temp ID with real ID from server
346
+ const cache = useGraphQLCache();
347
+ cache.update("AllFilms", {}, (current) => ({
348
+ films: current?.films.map(f => f.id === 'temp' ? data.addFilm : f) ?? []
349
+ }));
350
+ },
351
+
352
+ onSettled: (result, variables, context) => {
353
+ // Always runs after mutation (success or error)
354
+ console.log('Mutation completed');
355
+ }
356
+ });
357
+
358
+ const result = await mutate({ title: "New Film" });
305
359
  ```
306
360
 
307
361
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
3
  "configKey": "graphql",
4
- "version": "0.6.11",
4
+ "version": "0.6.12",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -203,7 +203,7 @@ function addUniversalTemplate({ filename, getContents, emitTs }) {
203
203
  return modulePath;
204
204
  }
205
205
 
206
- const version = "0.6.11";
206
+ const version = "0.6.12";
207
207
 
208
208
  async function getDocuments(documentsGlob) {
209
209
  try {
@@ -1,13 +1,14 @@
1
- import type { QueryName } from "#graphql/registry";
1
+ import type { QueryName, ResultOf, VariablesOf } from "#graphql/registry";
2
+ import type { IsEmptyObject } from "../../shared/lib/types.js";
2
3
  /**
3
4
  * GraphQL cache helper composable.
4
5
  *
5
- * @returns Cache config and invalidation helper.
6
+ * @returns Cache manipulation helpers.
6
7
  */
7
8
  export declare function useGraphQLCache(): {
8
- readonly cacheConfig: any;
9
- readonly invalidate: (options?: {
10
- operation: QueryName;
11
- variables?: unknown;
12
- }) => Promise<void>;
9
+ readonly cacheConfig: import("../../shared/lib/types.js").CacheConfig;
10
+ readonly read: <TName extends QueryName>(operation: TName, ...args: IsEmptyObject<VariablesOf<TName>> extends true ? [variables?: VariablesOf<TName>] : [variables: VariablesOf<TName>]) => ResultOf<TName> | undefined;
11
+ readonly write: <TName extends QueryName>(operation: TName, variables: VariablesOf<TName>, value: ResultOf<TName> | ((current: ResultOf<TName> | undefined) => ResultOf<TName>)) => void;
12
+ readonly update: <TName extends QueryName>(operation: TName, variables: VariablesOf<TName>, value: ResultOf<TName> | ((current: ResultOf<TName> | undefined) => ResultOf<TName>)) => Promise<void>;
13
+ readonly invalidate: <TName extends QueryName>(operation?: TName | undefined, variables?: VariablesOf<TName> | undefined) => Promise<void>;
13
14
  };
@@ -1,17 +1,40 @@
1
- import { clearNuxtData, useRuntimeConfig } from "#imports";
1
+ import { clearNuxtData, useNuxtData, useRuntimeConfig } from "#imports";
2
2
  import { getCacheKeyParts } from "../lib/cache.js";
3
- import { deletePersistedByPrefix, deletePersistedEntry } from "../lib/persisted.js";
3
+ import { deletePersistedByPrefix, deletePersistedEntry, getPersistedEntry, setPersistedEntry } from "../lib/persisted.js";
4
4
  export function useGraphQLCache() {
5
5
  const { public: { graphql: { cacheConfig } } } = useRuntimeConfig();
6
- async function invalidate(options) {
7
- if (!options) {
6
+ function read(operation, ...args) {
7
+ const [variables] = args;
8
+ const { key } = getCacheKeyParts(cacheConfig, operation, variables ?? {});
9
+ const nuxtData = useNuxtData(key);
10
+ return nuxtData.data.value;
11
+ }
12
+ function write(operation, variables, value) {
13
+ const { key } = getCacheKeyParts(cacheConfig, operation, variables);
14
+ const nuxtData = useNuxtData(key);
15
+ nuxtData.data.value = typeof value === "function" ? value(nuxtData.data.value) : value;
16
+ }
17
+ async function update(operation, variables, value) {
18
+ const { key } = getCacheKeyParts(cacheConfig, operation, variables);
19
+ const nuxtData = useNuxtData(key);
20
+ let current = nuxtData.data.value;
21
+ if (current === void 0 && cacheConfig.ttl !== void 0) {
22
+ current = await getPersistedEntry(key);
23
+ }
24
+ const updated = typeof value === "function" ? value(current) : value;
25
+ nuxtData.data.value = updated;
26
+ if (cacheConfig.ttl !== void 0) {
27
+ await setPersistedEntry(key, updated, cacheConfig.ttl);
28
+ }
29
+ }
30
+ async function invalidate(operation, variables) {
31
+ if (operation === void 0) {
8
32
  const { keyPrefix, keyVersion } = cacheConfig;
9
33
  const prefix = `${keyPrefix}:${keyVersion}:`;
10
34
  clearNuxtData((k) => k.startsWith(prefix));
11
35
  await deletePersistedByPrefix(prefix);
12
36
  return;
13
37
  }
14
- const { operation, variables } = options;
15
38
  if (variables === void 0) {
16
39
  const { opPrefix } = getCacheKeyParts(cacheConfig, operation, {});
17
40
  clearNuxtData((k) => k.startsWith(opPrefix));
@@ -22,5 +45,5 @@ export function useGraphQLCache() {
22
45
  clearNuxtData(key);
23
46
  await deletePersistedEntry(key);
24
47
  }
25
- return { cacheConfig, invalidate };
48
+ return { cacheConfig, read, write, update, invalidate };
26
49
  }
@@ -10,8 +10,8 @@ type Connection<TItem> = {
10
10
  };
11
11
  export declare function useGraphQLLoadMore<TQueryName extends QueryName, TConnection extends Connection<unknown>>(queryName: TQueryName, inputVars: MaybeRef<Omit<VariablesOf<TQueryName>, "after">>, getConnection: (data?: ResultOf<TQueryName>) => TConnection | undefined): Promise<{
12
12
  items: Ref<TConnection["nodes"][number][], TConnection["nodes"][number][]>;
13
- pending: any;
14
- error: any;
13
+ pending: Ref<boolean, boolean>;
14
+ error: Ref<import("../../shared/lib/error.js").NormalizedError | undefined, import("../../shared/lib/error.js").NormalizedError | undefined>;
15
15
  reset: (clearProducts?: boolean) => void;
16
16
  hasNextPage: ComputedRef<boolean>;
17
17
  isLoadingMore: Ref<boolean, boolean>;
@@ -1,6 +1,57 @@
1
1
  import type { MutationName, ResultOf, VariablesOf } from "#graphql/registry";
2
2
  import type { ExecuteGraphQLResult, IsEmptyObject } from "../../shared/lib/types.js";
3
- export declare function useGraphQLMutation<TName extends MutationName>(operationName: TName): {
3
+ import { type NormalizedError } from "../../shared/lib/error.js";
4
+ /**
5
+ * Mutation lifecycle hooks for optimistic updates and cache manipulation.
6
+ *
7
+ * @template TName Mutation operation name.
8
+ * @template TContext Type of context returned by onMutate and passed to other hooks.
9
+ */
10
+ export type MutationOptions<TName extends MutationName, TContext = unknown> = {
11
+ /**
12
+ * Callback invoked before the mutation executes.
13
+ * Use for optimistic updates. Return value is passed as context to other hooks.
14
+ *
15
+ * @param variables Mutation variables.
16
+ * @returns Context object for onSuccess/onError/onSettled hooks.
17
+ */
18
+ onMutate?: (variables: VariablesOf<TName>) => TContext | Promise<TContext>;
19
+ /**
20
+ * Callback invoked when the mutation succeeds.
21
+ *
22
+ * @param data Mutation result data.
23
+ * @param variables Mutation variables.
24
+ * @param context Context returned from onMutate (undefined if onMutate not provided or threw).
25
+ */
26
+ onSuccess?: (data: ResultOf<TName>, variables: VariablesOf<TName>, context: TContext | undefined) => void;
27
+ /**
28
+ * Callback invoked when the mutation fails.
29
+ * Use for rolling back optimistic updates.
30
+ *
31
+ * @param error Normalized error.
32
+ * @param variables Mutation variables.
33
+ * @param context Context returned from onMutate (undefined if onMutate not provided or threw).
34
+ */
35
+ onError?: (error: NormalizedError, variables: VariablesOf<TName>, context: TContext | undefined) => void;
36
+ /**
37
+ * Callback invoked when the mutation completes (success or error).
38
+ *
39
+ * @param result Mutation result (data or error).
40
+ * @param variables Mutation variables.
41
+ * @param context Context returned from onMutate (undefined if onMutate not provided or threw).
42
+ */
43
+ onSettled?: (result: ExecuteGraphQLResult<ResultOf<TName>>, variables: VariablesOf<TName>, context: TContext | undefined) => void;
44
+ };
45
+ /**
46
+ * GraphQL mutation composable with lifecycle hooks for optimistic updates.
47
+ *
48
+ * @template TName Mutation operation name.
49
+ * @template TContext Type of context returned by onMutate and passed to other hooks.
50
+ * @param operationName Mutation operation name.
51
+ * @param options Optional mutation lifecycle hooks.
52
+ * @returns Mutation state and mutate function.
53
+ */
54
+ export declare function useGraphQLMutation<TName extends MutationName, TContext = unknown>(operationName: TName, options?: MutationOptions<TName, TContext>): {
4
55
  pending: import("vue").Ref<boolean, boolean>;
5
56
  mutate: (...args: IsEmptyObject<VariablesOf<TName>> extends true ? [variables?: VariablesOf<TName>] : [variables: VariablesOf<TName>]) => Promise<ExecuteGraphQLResult<ResultOf<TName>>>;
6
57
  };
@@ -1,20 +1,42 @@
1
1
  import { useNuxtApp } from "#app";
2
2
  import { ref } from "vue";
3
3
  import { getOperationDocument } from "../../shared/lib/registry.js";
4
- export function useGraphQLMutation(operationName) {
4
+ import { normalizeError } from "../../shared/lib/error.js";
5
+ export function useGraphQLMutation(operationName, options) {
5
6
  const { $executeGraphQL } = useNuxtApp();
6
7
  const document = getOperationDocument(operationName);
7
8
  const pending = ref(false);
8
9
  async function mutate(...args) {
9
10
  const [variables] = args;
11
+ let context;
12
+ if (options?.onMutate) {
13
+ try {
14
+ context = await options.onMutate(variables);
15
+ } catch (error) {
16
+ const normalizedError = normalizeError(error);
17
+ options?.onError?.(normalizedError, variables, context);
18
+ return { data: null, error: normalizedError };
19
+ }
20
+ }
10
21
  pending.value = true;
11
22
  try {
12
- return await $executeGraphQL({ query: document, variables, operationName });
23
+ const result = await $executeGraphQL({ query: document, variables, operationName });
24
+ if (result.error) {
25
+ options?.onError?.(result.error, variables, context);
26
+ } else if (result.data) {
27
+ options?.onSuccess?.(result.data, variables, context);
28
+ }
29
+ options?.onSettled?.(result, variables, context);
30
+ return result;
31
+ } catch (error) {
32
+ const normalizedError = normalizeError(error);
33
+ const errorResult = { data: null, error: normalizedError };
34
+ options?.onError?.(normalizedError, variables, context);
35
+ options?.onSettled?.(errorResult, variables, context);
36
+ return errorResult;
13
37
  } finally {
14
38
  pending.value = false;
15
39
  }
16
40
  }
17
- ;
18
41
  return { pending, mutate };
19
42
  }
20
- ;
@@ -1,5 +1,9 @@
1
1
  import type { ExecuteGraphQLInput, ExecuteGraphQLResult, GraphQLVariables } from "../../shared/lib/types.js";
2
- declare const _default: any;
2
+ declare const _default: import("#app").Plugin<{
3
+ executeGraphQL: <TResult = unknown, TVariables extends GraphQLVariables = GraphQLVariables>({ query, variables, operationName }: ExecuteGraphQLInput<TVariables>) => Promise<ExecuteGraphQLResult<TResult>>;
4
+ }> & import("#app").ObjectPlugin<{
5
+ executeGraphQL: <TResult = unknown, TVariables extends GraphQLVariables = GraphQLVariables>({ query, variables, operationName }: ExecuteGraphQLInput<TVariables>) => Promise<ExecuteGraphQLResult<TResult>>;
6
+ }>;
3
7
  export default _default;
4
8
  type ExecuteGraphQL = <TResult = unknown, TVariables extends GraphQLVariables = GraphQLVariables>(input: ExecuteGraphQLInput<TVariables>) => Promise<ExecuteGraphQLResult<TResult>>;
5
9
  declare module "#app/nuxt" {
@@ -4,7 +4,11 @@ import { type Client as SSEClient } from "graphql-sse";
4
4
  *
5
5
  * @returns Nuxt plugin with SSE client provider.
6
6
  */
7
- declare const _default: any;
7
+ declare const _default: import("#app").Plugin<{
8
+ getGraphQLSSEClient: () => SSEClient;
9
+ }> & import("#app").ObjectPlugin<{
10
+ getGraphQLSSEClient: () => SSEClient;
11
+ }>;
8
12
  export default _default;
9
13
  declare module "#app/nuxt" {
10
14
  interface NuxtApp {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "description": "Opinionated Nuxt module for using GraphQL",
5
5
  "repository": "lewebsimple/nuxt-graphql",
6
6
  "license": "AGPL-3.0-only",
@@ -33,7 +33,7 @@
33
33
  "@graphql-tools/graphql-file-loader": "^8.1.9",
34
34
  "@graphql-tools/load": "^8.1.8",
35
35
  "@graphql-tools/schema": "^10.0.31",
36
- "@graphql-tools/stitch": "^10.1.10",
36
+ "@graphql-tools/stitch": "^10.1.11",
37
37
  "@nuxt/kit": "^4.3.1",
38
38
  "defu": "^6.1.4",
39
39
  "graphql": "^16.12.0",
@@ -44,7 +44,7 @@
44
44
  "unstorage": "^1.17.4"
45
45
  },
46
46
  "devDependencies": {
47
- "@nuxt/devtools": "^3.1.1",
47
+ "@nuxt/devtools": "^3.2.1",
48
48
  "@nuxt/eslint-config": "^1.15.1",
49
49
  "@nuxt/module-builder": "^1.0.2",
50
50
  "@nuxt/schema": "^4.3.1",