@lewebsimple/nuxt-graphql 0.6.11 → 0.6.14
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 +63 -9
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -3
- package/dist/runtime/app/composables/useGraphQLCache.client.d.ts +8 -7
- package/dist/runtime/app/composables/useGraphQLCache.client.js +29 -6
- package/dist/runtime/app/composables/useGraphQLLoadMore.d.ts +2 -2
- package/dist/runtime/app/composables/useGraphQLMutation.d.ts +52 -1
- package/dist/runtime/app/composables/useGraphQLMutation.js +26 -4
- package/dist/runtime/app/plugins/execute-graphql.d.ts +5 -1
- package/dist/runtime/app/plugins/graphql-sse.client.d.ts +5 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -287,21 +287,75 @@ const { data } = await useAsyncGraphQLQuery("HelloWorld", undefined, {
|
|
|
287
287
|
});
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
####
|
|
290
|
+
#### Cache manipulation
|
|
291
291
|
|
|
292
|
-
On the client, `useGraphQLCache()`
|
|
292
|
+
On the client, `useGraphQLCache()` provides helpers to read, write, update, and invalidate cache entries:
|
|
293
293
|
|
|
294
294
|
```ts
|
|
295
|
-
const
|
|
295
|
+
const cache = useGraphQLCache();
|
|
296
296
|
|
|
297
|
-
//
|
|
298
|
-
|
|
297
|
+
// Read cached query (in-memory only)
|
|
298
|
+
const films = cache.read("AllFilms", {});
|
|
299
299
|
|
|
300
|
-
//
|
|
301
|
-
|
|
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
|
-
//
|
|
304
|
-
await
|
|
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
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.
|
|
206
|
+
const version = "0.6.14";
|
|
207
207
|
|
|
208
208
|
async function getDocuments(documentsGlob) {
|
|
209
209
|
try {
|
|
@@ -239,7 +239,6 @@ async function getOperationsTemplate({ loadSchema, loadDocuments, documentGlob }
|
|
|
239
239
|
},
|
|
240
240
|
defaultScalarType: "never",
|
|
241
241
|
enumsAsTypes: true,
|
|
242
|
-
maybeValue: "T | undefined",
|
|
243
242
|
preResolveTypes: false,
|
|
244
243
|
strictScalars: true,
|
|
245
244
|
useTypeImports: true
|
|
@@ -257,7 +256,6 @@ async function getOperationsTemplate({ loadSchema, loadDocuments, documentGlob }
|
|
|
257
256
|
enumsAsTypes: true,
|
|
258
257
|
exportFragmentSpreadSubTypes: true,
|
|
259
258
|
inlineFragmentTypes: "combine",
|
|
260
|
-
maybeValue: "T | undefined",
|
|
261
259
|
operationResultSuffix: "Result",
|
|
262
260
|
operationVariablesSuffix: "Variables",
|
|
263
261
|
preResolveTypes: false,
|
|
@@ -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
|
|
6
|
+
* @returns Cache manipulation helpers.
|
|
6
7
|
*/
|
|
7
8
|
export declare function useGraphQLCache(): {
|
|
8
|
-
readonly cacheConfig:
|
|
9
|
-
readonly
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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:
|
|
14
|
-
error:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.6.14",
|
|
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.
|
|
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.
|
|
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",
|