@ramathibodi/nuxt-commons 4.0.13 → 4.0.15

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 (37) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/dialog/ImportResult.d.vue.ts +28 -0
  3. package/dist/runtime/components/dialog/ImportResult.vue +122 -0
  4. package/dist/runtime/components/dialog/ImportResult.vue.d.ts +28 -0
  5. package/dist/runtime/components/form/Birthdate.vue +7 -0
  6. package/dist/runtime/components/form/CheckboxGroup.d.vue.ts +1 -1
  7. package/dist/runtime/components/form/CheckboxGroup.vue +47 -46
  8. package/dist/runtime/components/form/CheckboxGroup.vue.d.ts +1 -1
  9. package/dist/runtime/components/form/Date.d.vue.ts +8 -1
  10. package/dist/runtime/components/form/Date.vue +9 -2
  11. package/dist/runtime/components/form/Date.vue.d.ts +8 -1
  12. package/dist/runtime/components/form/Time.d.vue.ts +1 -1
  13. package/dist/runtime/components/form/Time.vue +2 -1
  14. package/dist/runtime/components/form/Time.vue.d.ts +1 -1
  15. package/dist/runtime/components/model/Table.d.vue.ts +27 -18
  16. package/dist/runtime/components/model/Table.vue +12 -0
  17. package/dist/runtime/components/model/Table.vue.d.ts +27 -18
  18. package/dist/runtime/components/model/iterator.d.vue.ts +33 -24
  19. package/dist/runtime/components/model/iterator.vue +12 -0
  20. package/dist/runtime/components/model/iterator.vue.d.ts +33 -24
  21. package/dist/runtime/composables/api.d.ts +7 -0
  22. package/dist/runtime/composables/api.js +3 -2
  23. package/dist/runtime/composables/apiModel.d.ts +11 -2
  24. package/dist/runtime/composables/apiModel.js +10 -9
  25. package/dist/runtime/composables/apiModelOperation.d.ts +17 -3
  26. package/dist/runtime/composables/apiModelOperation.js +6 -6
  27. package/dist/runtime/composables/graphql.d.ts +3 -3
  28. package/dist/runtime/composables/graphql.js +6 -6
  29. package/dist/runtime/composables/graphqlModel.d.ts +11 -2
  30. package/dist/runtime/composables/graphqlModel.js +8 -8
  31. package/dist/runtime/composables/graphqlOperation.d.ts +2 -1
  32. package/dist/runtime/composables/graphqlOperation.js +3 -3
  33. package/dist/runtime/composables/importProgress.d.ts +25 -1
  34. package/dist/runtime/composables/importProgress.js +84 -4
  35. package/dist/runtime/types/graphqlOperation.d.ts +12 -1
  36. package/package.json +1 -1
  37. package/templates/.codegen/plugin-schema-object.cjs +10 -13
@@ -64,6 +64,13 @@ export type CacheOption = boolean | number | {
64
64
  */
65
65
  export interface ApiIdempotencyOption {
66
66
  idempotent?: boolean;
67
+ /**
68
+ * Optional discriminator folded as a trailing component into the
69
+ * Idempotency-Key hash: SHA-256(`<second>|<username>|<body>|<salt>`).
70
+ * Lets legitimately-identical payloads (e.g. duplicate bulk-import rows)
71
+ * resolve to distinct keys without weakening dedup elsewhere.
72
+ */
73
+ idempotencySalt?: string | number;
67
74
  }
68
75
  export type ApiFetchOptions = UseFetchOptions<unknown> & ApiIdempotencyOption;
69
76
  export declare function _resetLegacyHeuristicWarning(): void;
@@ -101,9 +101,10 @@ export function useApi() {
101
101
  const username = (typeof auth.getUsername === "function" ? auth.getUsername() : auth.userProfile?.username) ?? "";
102
102
  const second = Math.floor(Date.now() / 1e3).toString();
103
103
  const bodyJson = stableStringify(body ?? {});
104
- headers[headerName] = await sha256(`${second}|${username}|${bodyJson}`);
104
+ const salt = options.idempotencySalt != null ? `|${String(options.idempotencySalt)}` : "";
105
+ headers[headerName] = await sha256(`${second}|${username}|${bodyJson}${salt}`);
105
106
  }
106
- const { idempotent: _omitIdempotent, ...passThroughOptions } = options;
107
+ const { idempotent: _omitIdempotent, idempotencySalt: _omitSalt, ...passThroughOptions } = options;
107
108
  const baseOptions = {
108
109
  method,
109
110
  body,
@@ -30,9 +30,9 @@ export declare function useApiModel<T extends ApiModelComposableProps>(props: T)
30
30
  canCreate: import("vue").ComputedRef<boolean>;
31
31
  canUpdate: import("vue").ComputedRef<boolean>;
32
32
  canDelete: import("vue").ComputedRef<boolean>;
33
- createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean) => Promise<any>;
33
+ createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean, idempotencySalt?: string | number) => Promise<any>;
34
34
  importItems: (importData: Record<string, any>[], callback?: FormDialogCallback) => void;
35
- updateItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<any>;
35
+ updateItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean, idempotencySalt?: string | number) => Promise<any>;
36
36
  deleteItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<any>;
37
37
  loadItems: (options: any) => Promise<void>;
38
38
  reload: () => Promise<void>;
@@ -45,15 +45,24 @@ export declare function useApiModel<T extends ApiModelComposableProps>(props: T)
45
45
  failed: import("vue").Ref<number, number>;
46
46
  errors: import("vue").Ref<{
47
47
  index: number;
48
+ item: any;
49
+ errorType: import("./importProgress.js").ImportErrorType;
48
50
  message: string;
51
+ detail?: any;
49
52
  }[], import("./importProgress.js").ImportError[] | {
50
53
  index: number;
54
+ item: any;
55
+ errorType: import("./importProgress.js").ImportErrorType;
51
56
  message: string;
57
+ detail?: any;
52
58
  }[]>;
53
59
  percent: import("vue").ComputedRef<number>;
60
+ resultVisible: import("vue").Ref<boolean, boolean>;
54
61
  reset: () => void;
55
62
  run: <T_1 = any>(items: T_1[], worker: import("./importProgress.js").ImportWorker<T_1>, options?: {
56
63
  concurrency?: number;
57
64
  }) => Promise<import("./importProgress.js").ImportSummary>;
65
+ retry: (indices?: number[]) => Promise<import("./importProgress.js").ImportSummary>;
66
+ dismissResult: () => void;
58
67
  };
59
68
  };
@@ -50,9 +50,9 @@ export function useApiModel(props) {
50
50
  }
51
51
  return [.../* @__PURE__ */ new Set([...fieldsProps, ...tmpFields])];
52
52
  });
53
- function createItem(item, callback, importing = false) {
53
+ function createItem(item, callback, importing = false, idempotencySalt) {
54
54
  isLoading.value = true;
55
- return ops.value.create(item, fields.value).then((result) => {
55
+ return ops.value.create(item, fields.value, idempotencySalt != null ? { idempotencySalt } : void 0).then((result) => {
56
56
  if (canServerPageable.value) {
57
57
  if (!importing) loadItems(currentOptions.value);
58
58
  } else {
@@ -75,15 +75,15 @@ export function useApiModel(props) {
75
75
  return;
76
76
  }
77
77
  isLoading.value = true;
78
- const worker = (item) => {
79
- const createAsNew = () => createItem(Object.assign({}, props.initialData, item), void 0, true);
80
- return item[props.modelKey || "id"] ? updateItem(item, void 0).then((result) => {
78
+ const worker = (item, _index, salt) => {
79
+ const createAsNew = () => createItem(Object.assign({}, props.initialData, item), void 0, true, salt);
80
+ return item[props.modelKey || "id"] ? updateItem(item, void 0, true, salt).then((result) => {
81
81
  if (!result) return createAsNew();
82
82
  }) : createAsNew();
83
83
  };
84
84
  importProgress.run(importData, worker, { concurrency: props.importConcurrency }).then(({ succeeded, failed }) => {
85
85
  if (failed > 0) {
86
- alert?.addAlert({ alertType: "warning", message: `\u0E19\u0E33\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 ${succeeded} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23, \u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27 ${failed} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
86
+ importProgress.resultVisible.value = true;
87
87
  } else {
88
88
  alert?.addAlert({ alertType: "success", message: `\u0E19\u0E33\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 ${succeeded} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
89
89
  }
@@ -93,9 +93,9 @@ export function useApiModel(props) {
93
93
  if (callback) callback.done();
94
94
  });
95
95
  }
96
- function updateItem(item, callback) {
96
+ function updateItem(item, callback, importing = false, idempotencySalt) {
97
97
  isLoading.value = true;
98
- return ops.value.update(item, fields.value).then((result) => {
98
+ return ops.value.update(item, fields.value, idempotencySalt != null ? { idempotencySalt } : void 0).then((result) => {
99
99
  if (canServerPageable.value) {
100
100
  loadItems(currentOptions.value);
101
101
  } else {
@@ -107,9 +107,10 @@ export function useApiModel(props) {
107
107
  if (callback && callback.setData) callback.setData(result);
108
108
  return result;
109
109
  }).catch((error) => {
110
+ if (importing) throw error;
110
111
  alert?.addAlert({ alertType: "error", message: error?.message || String(error) });
111
112
  }).finally(() => {
112
- isLoading.value = false;
113
+ if (!importing) isLoading.value = false;
113
114
  if (callback) callback.done();
114
115
  });
115
116
  }
@@ -1,3 +1,17 @@
1
+ /**
2
+ * useApiModelOperation provides a single runtime layer that owns every
3
+ * URL- and body-building rule for API mode (the REST counterpart to
4
+ * useGraphQlOperation). Reads preserve the full modelName; mutations
5
+ * strip the `By<Qualifier>` suffix.
6
+ *
7
+ * When the GraphQL bridge has a `graphqlType[modelName]` entry, fields
8
+ * are expanded via the existing buildFields() helper exactly the way
9
+ * useGraphQlOperation does for GraphQL queries. Otherwise, fields and
10
+ * variables pass through verbatim.
11
+ *
12
+ * This doc block is consumed by vue-docgen for generated API documentation.
13
+ */
14
+ import { type ApiIdempotencyOption } from './api.js';
1
15
  export interface ApiModelPageable {
2
16
  page?: number;
3
17
  perPage?: number;
@@ -12,8 +26,8 @@ export interface ApiModelOperationCallables<TItem = any, TInput = any> {
12
26
  };
13
27
  }>;
14
28
  search: <T = TItem>(fields?: Array<string | object>, variables?: Record<string, any>, search?: any) => Promise<T[]>;
15
- create: <T = TItem>(input: TInput | Record<string, any>, fields?: Array<string | object>) => Promise<T>;
16
- update: <T = TItem>(input: TInput | Record<string, any>, fields?: Array<string | object>) => Promise<T>;
17
- delete: <T = any>(input: TInput | Record<string, any>, fields?: Array<string | object>) => Promise<T>;
29
+ create: <T = TItem>(input: TInput | Record<string, any>, fields?: Array<string | object>, options?: ApiIdempotencyOption) => Promise<T>;
30
+ update: <T = TItem>(input: TInput | Record<string, any>, fields?: Array<string | object>, options?: ApiIdempotencyOption) => Promise<T>;
31
+ delete: <T = any>(input: TInput | Record<string, any>, fields?: Array<string | object>, options?: ApiIdempotencyOption) => Promise<T>;
18
32
  }
19
33
  export declare function useApiModelOperation<TItem = any, TInput = any>(modelName: string): ApiModelOperationCallables<TItem, TInput>;
@@ -40,17 +40,17 @@ export function useApiModelOperation(modelName) {
40
40
  search
41
41
  });
42
42
  },
43
- create: (input, fields) => {
43
+ create: (input, fields, options) => {
44
44
  const endpoint = buildApiEndpoint(stripModelByQualifier(modelName), "create");
45
- return api.postPromise(endpoint, { input, fields: prepFields(fields) });
45
+ return api.postPromise(endpoint, { input, fields: prepFields(fields) }, void 0, options);
46
46
  },
47
- update: (input, fields) => {
47
+ update: (input, fields, options) => {
48
48
  const endpoint = buildApiEndpoint(stripModelByQualifier(modelName), "update");
49
- return api.postPromise(endpoint, { input, fields: prepFields(fields) });
49
+ return api.postPromise(endpoint, { input, fields: prepFields(fields) }, void 0, options);
50
50
  },
51
- delete: (input, fields) => {
51
+ delete: (input, fields, options) => {
52
52
  const endpoint = buildApiEndpoint(stripModelByQualifier(modelName), "delete");
53
- return api.postPromise(endpoint, { input, fields: prepFields(fields) });
53
+ return api.postPromise(endpoint, { input, fields: prepFields(fields) }, void 0, options);
54
54
  }
55
55
  };
56
56
  }
@@ -1,6 +1,6 @@
1
1
  import { type UseMutationReturn } from '@vue/apollo-composable';
2
2
  import { type ClassConstructor } from '../utils/object.js';
3
- import { type CacheOption } from './api.js';
3
+ import { type CacheOption, type ApiIdempotencyOption } from './api.js';
4
4
  declare type VariableOptions = {
5
5
  type?: string;
6
6
  name?: string;
@@ -12,8 +12,8 @@ declare type VariableOptions = {
12
12
  };
13
13
  export declare function useGraphQl(): {
14
14
  query: <T>(operation: string, fields: Array<string | Object> | ClassConstructor<any>, variables?: VariableOptions, cache?: boolean) => import("@vue/apollo-composable").UseQueryReturn<T, Record<string, never>>;
15
- queryPromise: <T>(operation: string, fields: Array<string | Object> | ClassConstructor<any>, variables?: VariableOptions, cache?: CacheOption) => Promise<T>;
15
+ queryPromise: <T>(operation: string, fields: Array<string | Object> | ClassConstructor<any>, variables?: VariableOptions, cache?: CacheOption, options?: ApiIdempotencyOption) => Promise<T>;
16
16
  mutation: <T>(operation: string, fields: Array<string | Object> | ClassConstructor<any>, variables?: VariableOptions) => UseMutationReturn<any, any>;
17
- mutationPromise: <T>(operation: string, fields: Array<string | Object> | ClassConstructor<any>, variables?: VariableOptions) => Promise<T>;
17
+ mutationPromise: <T>(operation: string, fields: Array<string | Object> | ClassConstructor<any>, variables?: VariableOptions, options?: ApiIdempotencyOption) => Promise<T>;
18
18
  };
19
19
  export {};
@@ -5,12 +5,12 @@ import { classAttributes, isClassConstructor } from "../utils/object.js";
5
5
  import { useApi } from "./api.js";
6
6
  import { useRuntimeConfig } from "#imports";
7
7
  export function useGraphQl() {
8
- function simplePromiseCall(operation, query2, variables, cache = false) {
8
+ function simplePromiseCall(operation, query2, variables, cache = false, options) {
9
9
  return new Promise((resolve, reject) => {
10
10
  useApi().postPromise(useRuntimeConfig().public.WS_GRAPHQL, {
11
11
  query: query2,
12
12
  variables
13
- }, void 0, void 0, cache).then((result) => {
13
+ }, void 0, options, cache).then((result) => {
14
14
  if (result.errors) reject(result.errors.map((error) => {
15
15
  return error.message;
16
16
  }).join("\n"));
@@ -35,20 +35,20 @@ export function useGraphQl() {
35
35
  };
36
36
  return useQuery(gql(builder.query), builder.variables, options);
37
37
  }
38
- function queryPromise(operation, fields, variables, cache = false) {
38
+ function queryPromise(operation, fields, variables, cache = false, options) {
39
39
  if (isClassConstructor(fields)) fields = classAttributes(fields);
40
40
  const builder = gqlQuery({ operation, fields, variables });
41
- return simplePromiseCall(operation, builder.query, builder.variables, cache);
41
+ return simplePromiseCall(operation, builder.query, builder.variables, cache, options);
42
42
  }
43
43
  function mutation(operation, fields, variables) {
44
44
  if (isClassConstructor(fields)) fields = classAttributes(fields);
45
45
  const builder = gqlMutation({ operation, fields, variables });
46
46
  return useMutation(gql(builder.query), { variables: builder.variables });
47
47
  }
48
- function mutationPromise(operation, fields, variables) {
48
+ function mutationPromise(operation, fields, variables, options) {
49
49
  if (isClassConstructor(fields)) fields = classAttributes(fields);
50
50
  const builder = gqlMutation({ operation, fields, variables });
51
- return simplePromiseCall(operation, builder.query, builder.variables);
51
+ return simplePromiseCall(operation, builder.query, builder.variables, false, options);
52
52
  }
53
53
  return { query, queryPromise, mutation, mutationPromise };
54
54
  }
@@ -25,9 +25,9 @@ export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T):
25
25
  canCreate: import("vue").ComputedRef<boolean>;
26
26
  canUpdate: import("vue").ComputedRef<boolean>;
27
27
  canDelete: import("vue").ComputedRef<boolean>;
28
- createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean) => Promise<void>;
28
+ createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean, idempotencySalt?: string | number) => Promise<void>;
29
29
  importItems: (importData: Record<string, any>[], callback?: FormDialogCallback) => void;
30
- updateItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<void>;
30
+ updateItem: (item: Record<string, any>, callback?: FormDialogCallback, idempotencySalt?: string | number) => Promise<void>;
31
31
  deleteItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<any>;
32
32
  loadItems: (options: any) => Promise<void> | undefined;
33
33
  reload: () => Promise<void> | undefined;
@@ -40,15 +40,24 @@ export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T):
40
40
  failed: import("vue").Ref<number, number>;
41
41
  errors: import("vue").Ref<{
42
42
  index: number;
43
+ item: any;
44
+ errorType: import("./importProgress.js").ImportErrorType;
43
45
  message: string;
46
+ detail?: any;
44
47
  }[], import("./importProgress.js").ImportError[] | {
45
48
  index: number;
49
+ item: any;
50
+ errorType: import("./importProgress.js").ImportErrorType;
46
51
  message: string;
52
+ detail?: any;
47
53
  }[]>;
48
54
  percent: import("vue").ComputedRef<number>;
55
+ resultVisible: import("vue").Ref<boolean, boolean>;
49
56
  reset: () => void;
50
57
  run: <T_1 = any>(items: T_1[], worker: import("./importProgress.js").ImportWorker<T_1>, options?: {
51
58
  concurrency?: number;
52
59
  }) => Promise<import("./importProgress.js").ImportSummary>;
60
+ retry: (indices?: number[]) => Promise<import("./importProgress.js").ImportSummary>;
61
+ dismissResult: () => void;
53
62
  };
54
63
  };
@@ -57,9 +57,9 @@ export function useGraphqlModel(props) {
57
57
  const canDelete = computed(() => {
58
58
  return !!operationDelete.value;
59
59
  });
60
- function createItem(item, callback, importing = false) {
60
+ function createItem(item, callback, importing = false, idempotencySalt) {
61
61
  isLoading.value = true;
62
- return operationCreate.value?.call(fields.value, { input: item }).then((result) => {
62
+ return operationCreate.value?.call(fields.value, { input: item }, void 0, idempotencySalt != null ? { idempotencySalt } : void 0).then((result) => {
63
63
  if (canServerPageable.value) {
64
64
  if (!importing) loadItems(currentOptions.value);
65
65
  } else items.value.push(result);
@@ -79,15 +79,15 @@ export function useGraphqlModel(props) {
79
79
  return;
80
80
  }
81
81
  isLoading.value = true;
82
- const worker = (item) => {
83
- const createAsNew = () => createItem(Object.assign({}, props.initialData, item), void 0, true);
84
- return item[props.modelKey || "id"] ? operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
82
+ const worker = (item, _index, salt) => {
83
+ const createAsNew = () => createItem(Object.assign({}, props.initialData, item), void 0, true, salt);
84
+ return item[props.modelKey || "id"] ? operationUpdate.value?.call(fields.value, { input: item }, void 0, salt != null ? { idempotencySalt: salt } : void 0).then((result) => {
85
85
  if (!result) return createAsNew();
86
86
  }) : createAsNew();
87
87
  };
88
88
  importProgress.run(importData, worker, { concurrency: props.importConcurrency }).then(({ succeeded, failed }) => {
89
89
  if (failed > 0) {
90
- alert?.addAlert({ alertType: "warning", message: `\u0E19\u0E33\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 ${succeeded} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23, \u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27 ${failed} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
90
+ importProgress.resultVisible.value = true;
91
91
  } else {
92
92
  alert?.addAlert({ alertType: "success", message: `\u0E19\u0E33\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 ${succeeded} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
93
93
  }
@@ -97,9 +97,9 @@ export function useGraphqlModel(props) {
97
97
  if (callback) callback.done();
98
98
  });
99
99
  }
100
- function updateItem(item, callback) {
100
+ function updateItem(item, callback, idempotencySalt) {
101
101
  isLoading.value = true;
102
- return operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
102
+ return operationUpdate.value?.call(fields.value, { input: item }, void 0, idempotencySalt != null ? { idempotencySalt } : void 0).then((result) => {
103
103
  if (canServerPageable.value) loadItems(currentOptions.value);
104
104
  else {
105
105
  const index = items.value.findIndex((item2) => item2[props.modelKey || "id"] === result[props.modelKey || "id"]);
@@ -1,5 +1,6 @@
1
1
  import { type ClassConstructor } from '../utils/object.js';
2
2
  import type { graphqlTypeObject, graphqlVariable } from '../types/graphqlOperation.js';
3
+ import type { ApiIdempotencyOption } from './api.js';
3
4
  export declare function buildFields(operationFields: graphqlTypeObject, fields?: Array<string | Object> | ClassConstructor<any>, depth?: number): Array<string | Object>;
4
5
  export declare function buildVariables(outputVariables?: graphqlVariable[], inputVariables?: {
5
6
  [p: string]: any;
@@ -7,4 +8,4 @@ export declare function buildVariables(outputVariables?: graphqlVariable[], inpu
7
8
  export declare function buildRequiredInputFields(variables: graphqlVariable[] | undefined, inputField?: string): string[];
8
9
  export declare function useGraphQlOperation<T>(operationType: string, operation: string, fields?: Array<string | Object> | ClassConstructor<any>, variables?: {
9
10
  [p: string]: any;
10
- }, cache?: boolean | number): Promise<T>;
11
+ }, cache?: boolean | number, options?: ApiIdempotencyOption): Promise<T>;
@@ -93,7 +93,7 @@ export function buildRequiredInputFields(variables, inputField = "input") {
93
93
  return [];
94
94
  }
95
95
  }
96
- export function useGraphQlOperation(operationType, operation, fields, variables, cache = false) {
96
+ export function useGraphQlOperation(operationType, operation, fields, variables, cache = false, options) {
97
97
  const { graphqlOperation } = useGraphqlBridge();
98
98
  return new Promise((resolve, reject) => {
99
99
  let rejectReason = void 0;
@@ -107,8 +107,8 @@ export function useGraphQlOperation(operationType, operation, fields, variables,
107
107
  }
108
108
  if (graphqlOperation[operation]?.fields) {
109
109
  fields = buildFields(graphqlOperation[operation]?.fields, fields, 0);
110
- if (operationType == "Query") useGraphQl().queryPromise(operation, fields, variables, cache).then((result) => resolve(result)).catch((error) => reject(error));
111
- else if (operationType == "Mutation") useGraphQl().mutationPromise(operation, fields, variables).then((result) => resolve(result)).catch((error) => reject(error));
110
+ if (operationType == "Query") useGraphQl().queryPromise(operation, fields, variables, cache, options).then((result) => resolve(result)).catch((error) => reject(error));
111
+ else if (operationType == "Mutation") useGraphQl().mutationPromise(operation, fields, variables, options).then((result) => resolve(result)).catch((error) => reject(error));
112
112
  else reject("Invalid Operation Type");
113
113
  } else {
114
114
  reject("No schema of return data available");
@@ -1,13 +1,28 @@
1
+ export type ImportErrorType = 'network' | 'timeout' | 'server' | 'unknown';
1
2
  export interface ImportError {
2
3
  index: number;
4
+ item: any;
5
+ errorType: ImportErrorType;
3
6
  message: string;
7
+ detail?: any;
4
8
  }
5
9
  export interface ImportSummary {
6
10
  succeeded: number;
7
11
  failed: number;
8
12
  errors: ImportError[];
9
13
  }
10
- export type ImportWorker<T = any> = (item: T, index: number) => Promise<any>;
14
+ export type ImportWorker<T = any> = (item: T, index: number, salt: string) => Promise<any>;
15
+ /**
16
+ * classifyImportError buckets a thrown import error into a coarse type and
17
+ * extracts a short message + raw detail, normalising ofetch FetchError and
18
+ * Apollo error shapes so callers can separate connection failures from
19
+ * server-returned errors.
20
+ */
21
+ export declare function classifyImportError(error: any): {
22
+ errorType: ImportErrorType;
23
+ message: string;
24
+ detail?: any;
25
+ };
11
26
  /**
12
27
  * useImportProgress drives a concurrency-limited bulk import while exposing
13
28
  * reactive progress. The runner continues on per-row errors and never rejects;
@@ -21,14 +36,23 @@ export declare function useImportProgress(): {
21
36
  failed: import("vue").Ref<number, number>;
22
37
  errors: import("vue").Ref<{
23
38
  index: number;
39
+ item: any;
40
+ errorType: ImportErrorType;
24
41
  message: string;
42
+ detail?: any;
25
43
  }[], ImportError[] | {
26
44
  index: number;
45
+ item: any;
46
+ errorType: ImportErrorType;
27
47
  message: string;
48
+ detail?: any;
28
49
  }[]>;
29
50
  percent: import("vue").ComputedRef<number>;
51
+ resultVisible: import("vue").Ref<boolean, boolean>;
30
52
  reset: () => void;
31
53
  run: <T = any>(items: T[], worker: ImportWorker<T>, options?: {
32
54
  concurrency?: number;
33
55
  }) => Promise<ImportSummary>;
56
+ retry: (indices?: number[]) => Promise<ImportSummary>;
57
+ dismissResult: () => void;
34
58
  };
@@ -1,5 +1,26 @@
1
1
  import { computed, ref } from "vue";
2
2
  import pLimit from "p-limit";
3
+ export function classifyImportError(error) {
4
+ const isString = typeof error === "string";
5
+ const rawMessage = error?.message || (isString ? error : String(error));
6
+ const name = error?.name;
7
+ const status = error?.status ?? error?.statusCode ?? error?.response?.status;
8
+ const graphQLErrors = error?.graphQLErrors;
9
+ const networkError = error?.networkError;
10
+ if (name === "AbortError" || name === "TimeoutError" || /timeout|timed out|aborted/i.test(rawMessage)) {
11
+ return { errorType: "timeout", message: rawMessage, detail: error?.data };
12
+ }
13
+ if (networkError || /failed to fetch|fetch failed|network ?error|load failed/i.test(rawMessage)) {
14
+ return { errorType: "network", message: rawMessage, detail: networkError ?? error?.data };
15
+ }
16
+ if (status || Array.isArray(graphQLErrors) && graphQLErrors.length > 0 || isString) {
17
+ const detail = error?.data ?? (graphQLErrors?.length ? graphQLErrors : isString ? error : error?.response?._data);
18
+ const message = error?.data?.message || graphQLErrors?.[0]?.message || rawMessage || `Server error ${status ?? ""}`.trim();
19
+ return { errorType: "server", message, detail };
20
+ }
21
+ return { errorType: "unknown", message: rawMessage, detail: error?.data };
22
+ }
23
+ let _importRunSeq = 0;
3
24
  export function useImportProgress() {
4
25
  const isImporting = ref(false);
5
26
  const total = ref(0);
@@ -7,6 +28,11 @@ export function useImportProgress() {
7
28
  const succeeded = ref(0);
8
29
  const failed = ref(0);
9
30
  const errors = ref([]);
31
+ const resultVisible = ref(false);
32
+ let _items = [];
33
+ let _worker = null;
34
+ let _concurrency = 3;
35
+ let _runId = 0;
10
36
  const percent = computed(
11
37
  () => total.value ? Math.round(processed.value / total.value * 100) : 0
12
38
  );
@@ -16,6 +42,7 @@ export function useImportProgress() {
16
42
  succeeded.value = 0;
17
43
  failed.value = 0;
18
44
  errors.value = [];
45
+ resultVisible.value = false;
19
46
  }
20
47
  async function run(items, worker, options = {}) {
21
48
  if (isImporting.value) {
@@ -24,17 +51,23 @@ export function useImportProgress() {
24
51
  reset();
25
52
  total.value = items.length;
26
53
  isImporting.value = true;
27
- const limit = pLimit(options.concurrency && options.concurrency > 0 ? options.concurrency : 3);
54
+ _items = items;
55
+ _worker = worker;
56
+ _concurrency = options.concurrency && options.concurrency > 0 ? options.concurrency : 3;
57
+ _runId = ++_importRunSeq;
58
+ const limit = pLimit(_concurrency);
28
59
  try {
29
60
  await Promise.all(
30
61
  items.map(
31
62
  (item, index) => limit(async () => {
32
63
  try {
33
- await worker(item, index);
64
+ await worker(item, index, `import:${_runId}:${index}`);
34
65
  succeeded.value++;
35
66
  } catch (error) {
67
+ const info = classifyImportError(error);
68
+ console.error("[import] row failed", { index, errorType: info.errorType, item, message: info.message, detail: info.detail });
36
69
  failed.value++;
37
- errors.value.push({ index, message: error?.message || String(error) });
70
+ errors.value.push({ index, item, ...info });
38
71
  } finally {
39
72
  processed.value++;
40
73
  }
@@ -46,5 +79,52 @@ export function useImportProgress() {
46
79
  }
47
80
  return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
48
81
  }
49
- return { isImporting, total, processed, succeeded, failed, errors, percent, reset, run };
82
+ function dismissResult() {
83
+ resultVisible.value = false;
84
+ }
85
+ async function retry(indices) {
86
+ if (isImporting.value || !_worker) {
87
+ return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
88
+ }
89
+ const requested = indices && indices.length ? indices : errors.value.map((e) => e.index);
90
+ const targets = requested.filter((i) => errors.value.some((e) => e.index === i));
91
+ if (targets.length === 0) {
92
+ return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
93
+ }
94
+ isImporting.value = true;
95
+ const limit = pLimit(_concurrency);
96
+ try {
97
+ await Promise.all(
98
+ targets.map(
99
+ (index) => limit(async () => {
100
+ const item = _items[index];
101
+ try {
102
+ await _worker(item, index, `import:${_runId}:${index}`);
103
+ const pos = errors.value.findIndex((e) => e.index === index);
104
+ if (pos !== -1) {
105
+ errors.value.splice(pos, 1);
106
+ failed.value = Math.max(0, failed.value - 1);
107
+ succeeded.value++;
108
+ }
109
+ } catch (error) {
110
+ const info = classifyImportError(error);
111
+ console.error("[import] row failed", { index, errorType: info.errorType, item, message: info.message, detail: info.detail });
112
+ const next = { index, item, ...info };
113
+ const pos = errors.value.findIndex((e) => e.index === index);
114
+ if (pos !== -1) errors.value[pos] = next;
115
+ else {
116
+ errors.value.push(next);
117
+ failed.value++;
118
+ }
119
+ }
120
+ })
121
+ )
122
+ );
123
+ } finally {
124
+ processed.value = succeeded.value + failed.value;
125
+ isImporting.value = false;
126
+ }
127
+ return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
128
+ }
129
+ return { isImporting, total, processed, succeeded, failed, errors, percent, resultVisible, reset, run, retry, dismissResult };
50
130
  }
@@ -12,7 +12,18 @@ export interface graphqlOperationObject<T, U> {
12
12
  fields?: graphqlTypeObject
13
13
  operationType: 'Query' | 'Mutation'
14
14
  name: string
15
- call: (fields?: Array<string | Object>, variables?: U, cache?: boolean | number) => Promise<T>
15
+ /**
16
+ * Execute this operation.
17
+ *
18
+ * The signature is shared by Query and Mutation operations:
19
+ * - `cache` applies to **Query** operations only (localStorage TTL / fetch policy).
20
+ * It is **ignored for Mutation** operations — mutations are never cached, so a
21
+ * value passed here has no effect.
22
+ * - `options.idempotencySalt` applies to **both** kinds: it is folded into the
23
+ * request's `Idempotency-Key` (`SHA-256(<second>|<username>|<body>|<salt>)`).
24
+ * Pass `options.idempotent: false` to skip the header for this call.
25
+ */
26
+ call: (fields?: Array<string | Object>, variables?: U, cache?: boolean | number, options?: { idempotent?: boolean, idempotencySalt?: string | number }) => Promise<T>
16
27
  }
17
28
 
18
29
  export interface graphqlTypeObject {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "4.0.13",
3
+ "version": "4.0.15",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -104,19 +104,22 @@ module.exports = {
104
104
  return `
105
105
  import * as graphQlClass from "~/types/graphql"
106
106
  import { useGraphQlOperation } from "#imports";
107
+ import type { ApiIdempotencyOption } from "#imports";
107
108
  import type {
108
109
  graphqlInputObject,
109
110
  graphqlOperationObject,
110
111
  graphqlTypeObject,
111
112
  graphqlVariable
112
113
  } from "#build/types/graphqlOperation";
113
-
114
+
114
115
  export const scalarType = ${JSON.stringify(scalarType)}
115
-
116
- function operationCall${'<' + 'T,U>'}(operationType:string, operation: string,fields?: ${'Array' + '<string | Object>'},variables?: U,cache: boolean | number = false) {
117
- return useGraphQlOperation${'<' + 'T>'}(operationType,operation,fields,variables as {[p: string] : any} | undefined,cache)
116
+
117
+ function operationCall${'<' + 'T,U>'}(operationType:string, operation: string,fields?: ${'Array' + '<string | Object>'},variables?: U,cache: boolean | number = false,
118
+ options?: ApiIdempotencyOption) {
119
+ return useGraphQlOperation${'<' + 'T>'}(operationType,operation,fields,variables as {[p: string] : any} | undefined,cache,
120
+ options)
118
121
  }
119
-
122
+
120
123
  function createGraphQLOperation${'<' + 'T,U>'}(operationType: "Query" | "Mutation",name: string,variables?: graphqlVariable[],fields?: graphqlTypeObject): graphqlOperationObject${'<' + 'T,U>'} {
121
124
  let returnGraphqlOperation : ${'Record' + '<string,any>'} = {
122
125
  operationType,
@@ -124,14 +127,8 @@ module.exports = {
124
127
  variables,
125
128
  fields,
126
129
  }
127
- if (operationType === "Mutation") {
128
- returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U): Promise${'<' + 'T>'} {
129
- return operationCall(operationType,name, fields, variables);
130
- }
131
- } else {
132
- returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U,cache: boolean | number = false): Promise${'<' + 'T>'} {
133
- return operationCall(operationType,name, fields, variables,cache);
134
- }
130
+ returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U, cache: boolean | number = false, options?: ApiIdempotencyOption): Promise${'<' + 'T>'} {
131
+ return operationCall(operationType,name, fields, variables, cache, options);
135
132
  }
136
133
  return returnGraphqlOperation as graphqlOperationObject${'<' + 'T,U>'}
137
134
  }