@ramathibodi/nuxt-commons 4.0.12 → 4.0.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.
Files changed (40) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/dialog/ImportProgress.d.vue.ts +35 -0
  3. package/dist/runtime/components/dialog/ImportProgress.vue +53 -0
  4. package/dist/runtime/components/dialog/ImportProgress.vue.d.ts +35 -0
  5. package/dist/runtime/components/dialog/ImportResult.d.vue.ts +28 -0
  6. package/dist/runtime/components/dialog/ImportResult.vue +122 -0
  7. package/dist/runtime/components/dialog/ImportResult.vue.d.ts +28 -0
  8. package/dist/runtime/components/document/TemplateBuilder.vue +112 -7
  9. package/dist/runtime/components/model/Pad.vue +2 -1
  10. package/dist/runtime/components/model/Table.d.vue.ts +104 -28
  11. package/dist/runtime/components/model/Table.vue +118 -3
  12. package/dist/runtime/components/model/Table.vue.d.ts +104 -28
  13. package/dist/runtime/components/model/iterator.d.vue.ts +146 -49
  14. package/dist/runtime/components/model/iterator.vue +129 -5
  15. package/dist/runtime/components/model/iterator.vue.d.ts +146 -49
  16. package/dist/runtime/composables/api.d.ts +7 -0
  17. package/dist/runtime/composables/api.js +3 -2
  18. package/dist/runtime/composables/apiModel.d.ts +31 -3
  19. package/dist/runtime/composables/apiModel.js +30 -21
  20. package/dist/runtime/composables/apiModelOperation.d.ts +17 -3
  21. package/dist/runtime/composables/apiModelOperation.js +6 -6
  22. package/dist/runtime/composables/document/template.d.ts +61 -0
  23. package/dist/runtime/composables/document/template.js +59 -0
  24. package/dist/runtime/composables/document/validateTemplate.d.ts +62 -0
  25. package/dist/runtime/composables/document/validateTemplate.js +378 -0
  26. package/dist/runtime/composables/graphql.d.ts +3 -3
  27. package/dist/runtime/composables/graphql.js +6 -6
  28. package/dist/runtime/composables/graphqlModel.d.ts +31 -3
  29. package/dist/runtime/composables/graphqlModel.js +28 -20
  30. package/dist/runtime/composables/graphqlModelOperation.d.ts +1 -0
  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 +58 -0
  34. package/dist/runtime/composables/importProgress.js +130 -0
  35. package/dist/runtime/types/graphqlOperation.d.ts +12 -1
  36. package/dist/runtime/utils/virtualize.d.ts +15 -0
  37. package/dist/runtime/utils/virtualize.js +10 -0
  38. package/package.json +2 -1
  39. package/scripts/validate-document-template.mjs +158 -0
  40. package/templates/.codegen/plugin-schema-object.cjs +10 -13
@@ -25,11 +25,39 @@ 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>;
29
- importItems: (importItems: Record<string, any>[], callback?: FormDialogCallback) => void;
30
- updateItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<void>;
28
+ createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean, idempotencySalt?: string | number) => Promise<void>;
29
+ importItems: (importData: Record<string, any>[], callback?: FormDialogCallback) => 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;
34
34
  isLoading: import("vue").Ref<boolean, boolean>;
35
+ importProgress: {
36
+ isImporting: import("vue").Ref<boolean, boolean>;
37
+ total: import("vue").Ref<number, number>;
38
+ processed: import("vue").Ref<number, number>;
39
+ succeeded: import("vue").Ref<number, number>;
40
+ failed: import("vue").Ref<number, number>;
41
+ errors: import("vue").Ref<{
42
+ index: number;
43
+ item: any;
44
+ errorType: import("./importProgress.js").ImportErrorType;
45
+ message: string;
46
+ detail?: any;
47
+ }[], import("./importProgress.js").ImportError[] | {
48
+ index: number;
49
+ item: any;
50
+ errorType: import("./importProgress.js").ImportErrorType;
51
+ message: string;
52
+ detail?: any;
53
+ }[]>;
54
+ percent: import("vue").ComputedRef<number>;
55
+ resultVisible: import("vue").Ref<boolean, boolean>;
56
+ reset: () => void;
57
+ run: <T_1 = any>(items: T_1[], worker: import("./importProgress.js").ImportWorker<T_1>, options?: {
58
+ concurrency?: number;
59
+ }) => Promise<import("./importProgress.js").ImportSummary>;
60
+ retry: (indices?: number[]) => Promise<import("./importProgress.js").ImportSummary>;
61
+ dismissResult: () => void;
62
+ };
35
63
  };
@@ -3,7 +3,7 @@ import { watchDebounced } from "@vueuse/core";
3
3
  import { useAlert } from "./alert.js";
4
4
  import { buildRequiredInputFields } from "./graphqlOperation.js";
5
5
  import { useGraphqlModelOperation } from "./graphqlModelOperation.js";
6
- import pLimit from "p-limit";
6
+ import { useImportProgress } from "./importProgress.js";
7
7
  import { arrayWrap } from "../utils/array.js";
8
8
  export function useGraphqlModel(props) {
9
9
  const alert = useAlert();
@@ -15,6 +15,7 @@ export function useGraphqlModel(props) {
15
15
  search.value = keyword;
16
16
  }
17
17
  const isLoading = ref(false);
18
+ const importProgress = useImportProgress();
18
19
  const { operationCreate, operationUpdate, operationDelete, operationRead, operationReadPageable, operationSearch } = useGraphqlModelOperation(props);
19
20
  function keyToField(key) {
20
21
  const parts = key.split(".");
@@ -56,43 +57,49 @@ export function useGraphqlModel(props) {
56
57
  const canDelete = computed(() => {
57
58
  return !!operationDelete.value;
58
59
  });
59
- function createItem(item, callback, importing = false) {
60
+ function createItem(item, callback, importing = false, idempotencySalt) {
60
61
  isLoading.value = true;
61
- 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) => {
62
63
  if (canServerPageable.value) {
63
64
  if (!importing) loadItems(currentOptions.value);
64
65
  } else items.value.push(result);
65
66
  if (callback && callback.setData) callback.setData(result);
66
67
  }).catch((error) => {
68
+ if (importing) throw error;
67
69
  alert?.addAlert({ alertType: "error", message: error });
68
70
  }).finally(() => {
69
71
  if (!importing) isLoading.value = false;
70
72
  if (callback) callback.done();
71
73
  });
72
74
  }
73
- function importItems(importItems2, callback) {
75
+ function importItems(importData, callback) {
76
+ if (importProgress.isImporting.value) return;
77
+ if (importData.length === 0) {
78
+ if (callback) callback.done();
79
+ return;
80
+ }
74
81
  isLoading.value = true;
75
- const limit = pLimit(50);
76
- const importPromises = importItems2.map(
77
- (item) => limit(
78
- () => (item[props.modelKey || "id"] ? operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
79
- if (!result) {
80
- return createItem(Object.assign({}, props.initialData, item), void 0, true);
81
- }
82
- }) : createItem(Object.assign({}, props.initialData, item), void 0, true)).catch((error) => {
83
- alert?.addAlert({ alertType: "error", message: error });
84
- })
85
- )
86
- );
87
- Promise.all(importPromises).finally(() => {
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
+ if (!result) return createAsNew();
86
+ }) : createAsNew();
87
+ };
88
+ importProgress.run(importData, worker, { concurrency: props.importConcurrency }).then(({ succeeded, failed }) => {
89
+ if (failed > 0) {
90
+ importProgress.resultVisible.value = true;
91
+ } else {
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
+ }
94
+ }).finally(() => {
88
95
  isLoading.value = false;
89
96
  reload();
90
97
  if (callback) callback.done();
91
98
  });
92
99
  }
93
- function updateItem(item, callback) {
100
+ function updateItem(item, callback, idempotencySalt) {
94
101
  isLoading.value = true;
95
- 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) => {
96
103
  if (canServerPageable.value) loadItems(currentOptions.value);
97
104
  else {
98
105
  const index = items.value.findIndex((item2) => item2[props.modelKey || "id"] === result[props.modelKey || "id"]);
@@ -184,6 +191,7 @@ export function useGraphqlModel(props) {
184
191
  deleteItem,
185
192
  loadItems,
186
193
  reload,
187
- isLoading
194
+ isLoading,
195
+ importProgress
188
196
  };
189
197
  }
@@ -10,6 +10,7 @@ export interface GraphqlModelConfigProps {
10
10
  operationReadPageable?: graphqlOperationObject<any, any> | string;
11
11
  operationSearch?: graphqlOperationObject<any, any> | string;
12
12
  fields?: Array<string | object>;
13
+ importConcurrency?: number;
13
14
  }
14
15
  export declare function useGraphqlModelOperation<T extends GraphqlModelConfigProps>(props: T): {
15
16
  operationCreate: import("vue").ComputedRef<graphqlOperationObject<any, any>>;
@@ -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");
@@ -0,0 +1,58 @@
1
+ export type ImportErrorType = 'network' | 'timeout' | 'server' | 'unknown';
2
+ export interface ImportError {
3
+ index: number;
4
+ item: any;
5
+ errorType: ImportErrorType;
6
+ message: string;
7
+ detail?: any;
8
+ }
9
+ export interface ImportSummary {
10
+ succeeded: number;
11
+ failed: number;
12
+ errors: ImportError[];
13
+ }
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
+ };
26
+ /**
27
+ * useImportProgress drives a concurrency-limited bulk import while exposing
28
+ * reactive progress. The runner continues on per-row errors and never rejects;
29
+ * callers read the resolved summary for the success/failure breakdown.
30
+ */
31
+ export declare function useImportProgress(): {
32
+ isImporting: import("vue").Ref<boolean, boolean>;
33
+ total: import("vue").Ref<number, number>;
34
+ processed: import("vue").Ref<number, number>;
35
+ succeeded: import("vue").Ref<number, number>;
36
+ failed: import("vue").Ref<number, number>;
37
+ errors: import("vue").Ref<{
38
+ index: number;
39
+ item: any;
40
+ errorType: ImportErrorType;
41
+ message: string;
42
+ detail?: any;
43
+ }[], ImportError[] | {
44
+ index: number;
45
+ item: any;
46
+ errorType: ImportErrorType;
47
+ message: string;
48
+ detail?: any;
49
+ }[]>;
50
+ percent: import("vue").ComputedRef<number>;
51
+ resultVisible: import("vue").Ref<boolean, boolean>;
52
+ reset: () => void;
53
+ run: <T = any>(items: T[], worker: ImportWorker<T>, options?: {
54
+ concurrency?: number;
55
+ }) => Promise<ImportSummary>;
56
+ retry: (indices?: number[]) => Promise<ImportSummary>;
57
+ dismissResult: () => void;
58
+ };
@@ -0,0 +1,130 @@
1
+ import { computed, ref } from "vue";
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;
24
+ export function useImportProgress() {
25
+ const isImporting = ref(false);
26
+ const total = ref(0);
27
+ const processed = ref(0);
28
+ const succeeded = ref(0);
29
+ const failed = ref(0);
30
+ const errors = ref([]);
31
+ const resultVisible = ref(false);
32
+ let _items = [];
33
+ let _worker = null;
34
+ let _concurrency = 3;
35
+ let _runId = 0;
36
+ const percent = computed(
37
+ () => total.value ? Math.round(processed.value / total.value * 100) : 0
38
+ );
39
+ function reset() {
40
+ total.value = 0;
41
+ processed.value = 0;
42
+ succeeded.value = 0;
43
+ failed.value = 0;
44
+ errors.value = [];
45
+ resultVisible.value = false;
46
+ }
47
+ async function run(items, worker, options = {}) {
48
+ if (isImporting.value) {
49
+ return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
50
+ }
51
+ reset();
52
+ total.value = items.length;
53
+ isImporting.value = true;
54
+ _items = items;
55
+ _worker = worker;
56
+ _concurrency = options.concurrency && options.concurrency > 0 ? options.concurrency : 3;
57
+ _runId = ++_importRunSeq;
58
+ const limit = pLimit(_concurrency);
59
+ try {
60
+ await Promise.all(
61
+ items.map(
62
+ (item, index) => limit(async () => {
63
+ try {
64
+ await worker(item, index, `import:${_runId}:${index}`);
65
+ succeeded.value++;
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 });
69
+ failed.value++;
70
+ errors.value.push({ index, item, ...info });
71
+ } finally {
72
+ processed.value++;
73
+ }
74
+ })
75
+ )
76
+ );
77
+ } finally {
78
+ isImporting.value = false;
79
+ }
80
+ return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
81
+ }
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 };
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 {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pure helpers for the ModelTable/ModelIterator virtualization layer (#246).
3
+ */
4
+ /**
5
+ * Decide whether to render rows virtualized.
6
+ * @param count rendered-row count (items.length)
7
+ * @param override tri-state `virtual` prop: true/false force, undefined = auto
8
+ * @param threshold auto-virtualize above this count (default 500)
9
+ */
10
+ export declare function shouldVirtualize(count: number, override: boolean | undefined, threshold: number | undefined): boolean;
11
+ /**
12
+ * Cards-per-row for a Vuetify grid column span (out of 12).
13
+ * span 12 -> 1, 6 -> 2, 4 -> 3, 2 -> 6. Falsy / out-of-range -> 1 (full width).
14
+ */
15
+ export declare function itemsPerRowForSpan(span: number | string | boolean | undefined): number;
@@ -0,0 +1,10 @@
1
+ export function shouldVirtualize(count, override, threshold) {
2
+ if (override === true) return true;
3
+ if (override === false) return false;
4
+ return count > (threshold ?? 500);
5
+ }
6
+ export function itemsPerRowForSpan(span) {
7
+ const n = Number(span);
8
+ if (!Number.isFinite(n) || n <= 0 || n > 12) return 1;
9
+ return Math.max(1, Math.floor(12 / n));
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramathibodi/nuxt-commons",
3
- "version": "4.0.12",
3
+ "version": "4.0.14",
4
4
  "description": "Ramathibodi Nuxt modules for common components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,6 +49,7 @@
49
49
  "dev:build": "nuxt build playground",
50
50
  "dev:generate": "nuxt generate playground",
51
51
  "playground:scaffold": "node scripts/scaffold-playground-pages.mjs",
52
+ "template:validate": "node scripts/validate-document-template.mjs",
52
53
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
53
54
  "docs:api:components": "vue-docgen -c docs/vue-docgen.config.cjs && npm run docs:ai:summary && node scripts/enrich-vue-docs-from-ai.mjs",
54
55
  "docs:api:composables": "typedoc --options docs/typedoc.json",
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Codegen-style validator for Document Template JSON.
4
+ *
5
+ * Usage:
6
+ * node scripts/validate-document-template.mjs [file|glob ...] [options]
7
+ * cat template.json | node scripts/validate-document-template.mjs
8
+ *
9
+ * Options:
10
+ * --strict Promote builder-unsafe warnings to errors (exit 1 on them too).
11
+ * --json Emit machine-readable JSON: [{ file, result }].
12
+ * --quiet Print errors only (suppress warnings and infos).
13
+ * -h, --help Show this help.
14
+ *
15
+ * Exit code: 1 if any input has errors (warnings count as errors under --strict),
16
+ * otherwise 0.
17
+ *
18
+ * Shares the exact validation core used by the `useDocumentTemplate` runtime via
19
+ * jiti (the TypeScript loader Nuxt already ships), so the CLI and the composable
20
+ * can never diverge.
21
+ */
22
+ import { readFileSync } from 'node:fs'
23
+ import { fileURLToPath } from 'node:url'
24
+ import { dirname, resolve, relative } from 'node:path'
25
+ import process from 'node:process'
26
+ import { createJiti } from 'jiti'
27
+
28
+ const dir = dirname(fileURLToPath(import.meta.url))
29
+ const repoRoot = resolve(dir, '..')
30
+
31
+ const jiti = createJiti(import.meta.url)
32
+ const { validateDocumentTemplate } = await jiti.import(
33
+ resolve(repoRoot, 'src/runtime/composables/document/validateTemplate.ts'),
34
+ )
35
+
36
+ // --- arg parsing -----------------------------------------------------------
37
+ const argv = process.argv.slice(2)
38
+ const flags = new Set(argv.filter(a => a.startsWith('-')))
39
+ const files = argv.filter(a => !a.startsWith('-'))
40
+
41
+ if (flags.has('-h') || flags.has('--help')) {
42
+ printHelp()
43
+ process.exit(0)
44
+ }
45
+
46
+ const strict = flags.has('--strict')
47
+ const asJson = flags.has('--json')
48
+ const quiet = flags.has('--quiet')
49
+
50
+ // --- input collection ------------------------------------------------------
51
+ /** @type {{ file: string, content: string }[]} */
52
+ const inputs = []
53
+
54
+ if (files.length === 0) {
55
+ const stdin = readFileSync(0, 'utf8')
56
+ if (!stdin.trim()) {
57
+ console.error('No input: pass file paths/globs or pipe JSON via stdin.')
58
+ process.exit(2)
59
+ }
60
+ inputs.push({ file: '<stdin>', content: stdin })
61
+ }
62
+ else {
63
+ const resolved = await expandGlobs(files)
64
+ if (resolved.length === 0) {
65
+ console.error(`No files matched: ${files.join(', ')}`)
66
+ process.exit(2)
67
+ }
68
+ for (const f of resolved) {
69
+ try {
70
+ inputs.push({ file: f, content: readFileSync(f, 'utf8') })
71
+ }
72
+ catch (e) {
73
+ console.error(`Cannot read ${f}: ${e.message}`)
74
+ process.exit(2)
75
+ }
76
+ }
77
+ }
78
+
79
+ // --- validation ------------------------------------------------------------
80
+ const results = inputs.map(({ file, content }) => ({
81
+ file,
82
+ result: validateDocumentTemplate(content, { strict }),
83
+ }))
84
+
85
+ const hadErrors = results.some(r => !r.result.valid)
86
+
87
+ if (asJson) {
88
+ const rel = f => (f === '<stdin>' ? f : relative(repoRoot, resolve(f)))
89
+ console.log(JSON.stringify(results.map(r => ({ file: rel(r.file), result: r.result })), null, 2))
90
+ process.exit(hadErrors ? 1 : 0)
91
+ }
92
+
93
+ // --- human report ----------------------------------------------------------
94
+ let totalErrors = 0
95
+ let totalWarnings = 0
96
+ let totalInfos = 0
97
+
98
+ for (const { file, result } of results) {
99
+ const shown = quiet ? result.errors : result.issues
100
+ totalErrors += result.errors.length
101
+ totalWarnings += result.warnings.length
102
+ totalInfos += result.infos.length
103
+
104
+ const status = result.valid ? (result.warnings.length ? 'WARN' : 'OK') : 'FAIL'
105
+ console.log(`\n${status} ${file}`)
106
+ if (shown.length === 0) {
107
+ if (!quiet) console.log(' no issues')
108
+ continue
109
+ }
110
+ for (const issue of shown) {
111
+ const tag = issue.severity.toUpperCase().padEnd(7)
112
+ const loc = issue.path || '(root)'
113
+ console.log(` ${tag} ${loc} ${issue.code} ${issue.message}`)
114
+ }
115
+ }
116
+
117
+ console.log(
118
+ `\n${results.length} file(s): ${totalErrors} error(s), ${totalWarnings} warning(s), ${totalInfos} info(s)`
119
+ + (strict ? ' [strict]' : ''),
120
+ )
121
+
122
+ process.exit(hadErrors ? 1 : 0)
123
+
124
+ // --- helpers ---------------------------------------------------------------
125
+ async function expandGlobs(patterns) {
126
+ const out = []
127
+ for (const p of patterns) {
128
+ if (/[*?[\]{}]/.test(p)) {
129
+ try {
130
+ const { glob } = await import('node:fs/promises')
131
+ for await (const entry of glob(p)) out.push(entry)
132
+ }
133
+ catch {
134
+ console.error(`Globbing not supported in this Node version for "${p}"; pass explicit file paths.`)
135
+ process.exit(2)
136
+ }
137
+ }
138
+ else {
139
+ out.push(p)
140
+ }
141
+ }
142
+ // De-dupe while preserving order.
143
+ return [...new Set(out)]
144
+ }
145
+
146
+ function printHelp() {
147
+ console.log(`Validate Document Template JSON.
148
+
149
+ Usage:
150
+ node scripts/validate-document-template.mjs [file|glob ...] [options]
151
+ cat template.json | node scripts/validate-document-template.mjs
152
+
153
+ Options:
154
+ --strict Promote builder-unsafe warnings to errors.
155
+ --json Emit machine-readable JSON.
156
+ --quiet Print errors only.
157
+ -h, --help Show this help.`)
158
+ }
@@ -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
  }