nuxt-openapi-hyperfetch 0.3.81-beta → 1.0.1
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 +220 -212
- package/dist/generators/components/connector-generator/templates.js +67 -17
- package/dist/generators/components/schema-analyzer/intent-detector.js +1 -12
- package/dist/generators/components/schema-analyzer/openapi-reader.js +10 -1
- package/dist/generators/components/schema-analyzer/resource-grouper.js +7 -0
- package/dist/generators/components/schema-analyzer/schema-field-mapper.js +1 -22
- package/dist/generators/components/schema-analyzer/types.d.ts +10 -0
- package/dist/generators/connectors/generator.d.ts +12 -0
- package/dist/generators/connectors/generator.js +115 -0
- package/dist/generators/connectors/runtime/connector-types.d.ts +147 -0
- package/dist/generators/connectors/runtime/connector-types.js +10 -0
- package/dist/generators/connectors/runtime/useCreateConnector.d.ts +26 -0
- package/dist/generators/connectors/runtime/useCreateConnector.js +156 -0
- package/dist/generators/connectors/runtime/useDeleteConnector.d.ts +30 -0
- package/dist/generators/connectors/runtime/useDeleteConnector.js +143 -0
- package/dist/generators/connectors/runtime/useGetAllConnector.d.ts +25 -0
- package/dist/generators/connectors/runtime/useGetAllConnector.js +127 -0
- package/dist/generators/connectors/runtime/useGetConnector.d.ts +15 -0
- package/dist/generators/connectors/runtime/useGetConnector.js +99 -0
- package/dist/generators/connectors/runtime/useUpdateConnector.d.ts +34 -0
- package/dist/generators/connectors/runtime/useUpdateConnector.js +211 -0
- package/dist/generators/connectors/runtime/zod-error-merger.d.ts +23 -0
- package/dist/generators/connectors/runtime/zod-error-merger.js +106 -0
- package/dist/generators/connectors/templates.d.ts +4 -0
- package/dist/generators/connectors/templates.js +376 -0
- package/dist/generators/connectors/types.d.ts +37 -0
- package/dist/generators/connectors/types.js +7 -0
- package/dist/generators/shared/runtime/useDeleteConnector.js +4 -2
- package/dist/generators/shared/runtime/useDetailConnector.d.ts +0 -1
- package/dist/generators/shared/runtime/useDetailConnector.js +9 -20
- package/dist/generators/shared/runtime/useFormConnector.js +4 -3
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +14 -5
- package/dist/generators/use-async-data/templates.js +20 -16
- package/dist/generators/use-fetch/templates.js +1 -1
- package/dist/index.js +1 -16
- package/dist/module/index.js +2 -3
- package/package.json +4 -3
- package/src/cli/prompts.ts +1 -7
- package/src/generators/components/connector-generator/templates.ts +97 -22
- package/src/generators/components/schema-analyzer/intent-detector.ts +1 -16
- package/src/generators/components/schema-analyzer/openapi-reader.ts +14 -1
- package/src/generators/components/schema-analyzer/resource-grouper.ts +9 -0
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +1 -26
- package/src/generators/components/schema-analyzer/types.ts +11 -0
- package/src/generators/connectors/generator.ts +137 -0
- package/src/generators/connectors/runtime/connector-types.ts +207 -0
- package/src/generators/connectors/runtime/useCreateConnector.ts +199 -0
- package/src/generators/connectors/runtime/useDeleteConnector.ts +179 -0
- package/src/generators/connectors/runtime/useGetAllConnector.ts +151 -0
- package/src/generators/connectors/runtime/useGetConnector.ts +120 -0
- package/src/generators/connectors/runtime/useUpdateConnector.ts +257 -0
- package/src/generators/connectors/runtime/zod-error-merger.ts +119 -0
- package/src/generators/connectors/templates.ts +481 -0
- package/src/generators/connectors/types.ts +39 -0
- package/src/generators/shared/runtime/useDeleteConnector.ts +4 -2
- package/src/generators/shared/runtime/useDetailConnector.ts +8 -19
- package/src/generators/shared/runtime/useFormConnector.ts +4 -3
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +16 -5
- package/src/generators/use-async-data/templates.ts +24 -16
- package/src/generators/use-fetch/templates.ts +1 -1
- package/src/index.ts +2 -19
- package/src/module/index.ts +2 -5
- package/docs/generated-components.md +0 -615
- package/docs/headless-composables-ui.md +0 -569
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// @ts-nocheck - This file runs in user's Nuxt project with different TypeScript config
|
|
2
|
+
/**
|
|
3
|
+
* useGetAllConnector — Runtime connector for list/collection GET endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Wraps a useAsyncData composable (reactive, SSR-compatible, supports pagination).
|
|
6
|
+
* The factory pattern means the composable is initialized in setup() context.
|
|
7
|
+
*
|
|
8
|
+
* Key differences from the old useListConnector:
|
|
9
|
+
* - items instead of rows
|
|
10
|
+
* - select/deselect/toggleSelect instead of onRowSelect
|
|
11
|
+
* - load(params?) instead of refresh()
|
|
12
|
+
* - No CRUD coordination methods (create/update/remove/_createTrigger etc.)
|
|
13
|
+
* — coordination is the component's responsibility via callbacks
|
|
14
|
+
*
|
|
15
|
+
* Copied to the user's project alongside the generated connectors.
|
|
16
|
+
*/
|
|
17
|
+
import { ref, computed } from 'vue';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param factory A zero-argument function that calls and returns the underlying
|
|
21
|
+
* useAsyncData composable, e.g. () => useAsyncDataGetPets(params)
|
|
22
|
+
* Called once during connector setup (inside setup()).
|
|
23
|
+
* @param options Configuration for the connector
|
|
24
|
+
*/
|
|
25
|
+
export function useGetAllConnector(factory, options = {}) {
|
|
26
|
+
const { columns = [], columnLabels = {}, columnLabel = null } = options;
|
|
27
|
+
|
|
28
|
+
// ── Execute the underlying composable once (in setup context) ─────────────
|
|
29
|
+
const composable = factory();
|
|
30
|
+
|
|
31
|
+
// ── Derived state ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const items = computed(() => {
|
|
34
|
+
const data = composable.data?.value;
|
|
35
|
+
if (!data) return [];
|
|
36
|
+
// Support both direct arrays and { data: [...] } shapes (paginated APIs)
|
|
37
|
+
if (Array.isArray(data)) return data;
|
|
38
|
+
if (Array.isArray(data.data)) return data.data;
|
|
39
|
+
return [];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const loading = computed(() => composable.pending?.value ?? false);
|
|
43
|
+
const error = computed(() => composable.error?.value ?? null);
|
|
44
|
+
|
|
45
|
+
// Pagination — passthrough from the underlying composable when paginated: true
|
|
46
|
+
const pagination = computed(() => composable.pagination?.value ?? null);
|
|
47
|
+
|
|
48
|
+
function goToPage(page) {
|
|
49
|
+
composable.pagination?.value?.goToPage?.(page);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nextPage() {
|
|
53
|
+
composable.pagination?.value?.nextPage?.();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function prevPage() {
|
|
57
|
+
composable.pagination?.value?.prevPage?.();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setPerPage(n) {
|
|
61
|
+
composable.pagination?.value?.setPerPage?.(n);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Selection ──────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const selected = ref([]);
|
|
67
|
+
|
|
68
|
+
function select(item) {
|
|
69
|
+
if (!selected.value.includes(item)) {
|
|
70
|
+
selected.value = [...selected.value, item];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function deselect(item) {
|
|
75
|
+
selected.value = selected.value.filter((r) => r !== item);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toggleSelect(item) {
|
|
79
|
+
if (selected.value.includes(item)) {
|
|
80
|
+
deselect(item);
|
|
81
|
+
} else {
|
|
82
|
+
select(item);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearSelection() {
|
|
87
|
+
selected.value = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Callbacks — developer-assignable ──────────────────────────────────────
|
|
91
|
+
const onSuccess = ref(null);
|
|
92
|
+
const onError = ref(null);
|
|
93
|
+
|
|
94
|
+
// ── Load / refresh ─────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reload the list. Equivalent to refresh() on the underlying composable.
|
|
98
|
+
* @param _params Reserved for future use (reactive params are handled by
|
|
99
|
+
* the underlying useAsyncData watch sources).
|
|
100
|
+
*/
|
|
101
|
+
async function load(_params) {
|
|
102
|
+
await composable.refresh?.();
|
|
103
|
+
if (!composable.error?.value) {
|
|
104
|
+
onSuccess.value?.(items.value);
|
|
105
|
+
} else {
|
|
106
|
+
onError.value?.(composable.error.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Column label resolution ────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const resolvedColumns = computed(() =>
|
|
113
|
+
columns.map((col) => ({
|
|
114
|
+
...col,
|
|
115
|
+
label: columnLabel
|
|
116
|
+
? columnLabel(col.key)
|
|
117
|
+
: (columnLabels[col.key] ?? col.label),
|
|
118
|
+
}))
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ── Return ─────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
// State
|
|
125
|
+
items,
|
|
126
|
+
columns: resolvedColumns,
|
|
127
|
+
loading,
|
|
128
|
+
error,
|
|
129
|
+
|
|
130
|
+
// Pagination
|
|
131
|
+
pagination,
|
|
132
|
+
goToPage,
|
|
133
|
+
nextPage,
|
|
134
|
+
prevPage,
|
|
135
|
+
setPerPage,
|
|
136
|
+
|
|
137
|
+
// Selection
|
|
138
|
+
selected,
|
|
139
|
+
select,
|
|
140
|
+
deselect,
|
|
141
|
+
toggleSelect,
|
|
142
|
+
clearSelection,
|
|
143
|
+
|
|
144
|
+
// Actions
|
|
145
|
+
load,
|
|
146
|
+
|
|
147
|
+
// Callbacks
|
|
148
|
+
onSuccess,
|
|
149
|
+
onError,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @ts-nocheck - This file runs in user's Nuxt project with different TypeScript config
|
|
2
|
+
/**
|
|
3
|
+
* useGetConnector — Runtime connector for single-item GET endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Uses $fetch directly (no useAsyncData) so:
|
|
6
|
+
* - load(id) is truly imperative and awaitable
|
|
7
|
+
* - Returns the fetched item from load()
|
|
8
|
+
* - No SSR key registration, no cache interference
|
|
9
|
+
*
|
|
10
|
+
* Copied to the user's project alongside the generated connectors.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, computed } from 'vue';
|
|
13
|
+
import { getGlobalBaseUrl, mergeCallbacks } from '../composables/shared/runtime/apiHelpers.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param urlFn URL string or function that receives an id and returns the URL string.
|
|
17
|
+
* e.g. '/pet/me' or (id) => `/pet/${id}`
|
|
18
|
+
* @param options Optional configuration
|
|
19
|
+
*/
|
|
20
|
+
export function useGetConnector(urlFn, options = {}) {
|
|
21
|
+
const resolveUrl = (id) => (typeof urlFn === 'function' ? urlFn(id) : urlFn);
|
|
22
|
+
const {
|
|
23
|
+
fields = [],
|
|
24
|
+
baseURL: baseURLOpt,
|
|
25
|
+
onRequest: onRequestOpt,
|
|
26
|
+
onSuccess: onSuccessOpt,
|
|
27
|
+
onError: onErrorOpt,
|
|
28
|
+
skipGlobalCallbacks,
|
|
29
|
+
} = options;
|
|
30
|
+
const baseURL = baseURLOpt || getGlobalBaseUrl();
|
|
31
|
+
if (!baseURL) {
|
|
32
|
+
console.warn('[useGetConnector] No baseURL configured. Set runtimeConfig.public.apiBaseUrl in nuxt.config.ts or pass baseURL in options.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Callbacks — developer-assignable (can also be passed as options)
|
|
36
|
+
// Both the connector-level option and the per-operation registration are called.
|
|
37
|
+
let _localOnSuccess = null;
|
|
38
|
+
let _localOnError = null;
|
|
39
|
+
|
|
40
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const data = ref(null);
|
|
43
|
+
const loading = ref(false);
|
|
44
|
+
const error = ref(null);
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// ── Actions ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetch a single item by ID.
|
|
51
|
+
* Returns the fetched item so it can be awaited imperatively:
|
|
52
|
+
* const pet = await get.load(5)
|
|
53
|
+
*/
|
|
54
|
+
async function load(id) {
|
|
55
|
+
if (id === undefined || id === null) {
|
|
56
|
+
console.warn('[useGetConnector] load() called with undefined/null id — request was not sent.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
loading.value = true;
|
|
60
|
+
error.value = null;
|
|
61
|
+
|
|
62
|
+
// Merge global + local callbacks (onRequest modifications, rule-based suppression)
|
|
63
|
+
const url = resolveUrl(id);
|
|
64
|
+
const merged = mergeCallbacks(url, 'GET', {
|
|
65
|
+
onRequest: onRequestOpt,
|
|
66
|
+
onSuccess: onSuccessOpt,
|
|
67
|
+
onError: onErrorOpt,
|
|
68
|
+
}, skipGlobalCallbacks);
|
|
69
|
+
|
|
70
|
+
// onRequest hook — collects header/query modifications
|
|
71
|
+
const requestMods = await merged.onRequest({ url, method: 'GET' });
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const result = await $fetch(url, {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
...(requestMods?.headers ? { headers: requestMods.headers } : {}),
|
|
77
|
+
...(requestMods?.query ? { query: requestMods.query } : {}),
|
|
78
|
+
...(baseURL ? { baseURL } : {}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
data.value = result;
|
|
82
|
+
await merged.onSuccess(result, { operation: 'get' });
|
|
83
|
+
_localOnSuccess?.(result);
|
|
84
|
+
return result;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
error.value = err;
|
|
87
|
+
await merged.onError(err, { operation: 'get' });
|
|
88
|
+
_localOnError?.(err);
|
|
89
|
+
throw err;
|
|
90
|
+
} finally {
|
|
91
|
+
loading.value = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear the current item from state.
|
|
97
|
+
*/
|
|
98
|
+
function clear() {
|
|
99
|
+
data.value = null;
|
|
100
|
+
error.value = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Return ─────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
// State
|
|
107
|
+
data,
|
|
108
|
+
loading,
|
|
109
|
+
error,
|
|
110
|
+
fields: computed(() => fields),
|
|
111
|
+
|
|
112
|
+
// Actions
|
|
113
|
+
load,
|
|
114
|
+
clear,
|
|
115
|
+
|
|
116
|
+
// Callbacks
|
|
117
|
+
onSuccess: (fn) => { _localOnSuccess = fn; },
|
|
118
|
+
onError: (fn) => { _localOnError = fn; },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// @ts-nocheck - This file runs in user's Nuxt project with different TypeScript config
|
|
2
|
+
/**
|
|
3
|
+
* useUpdateConnector — Runtime connector for PUT/PATCH endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Uses $fetch directly (no useAsyncData) so:
|
|
6
|
+
* - execute() always fires a real network request, no SSR cache interference
|
|
7
|
+
* - load(id) fetches the current item to pre-fill the form
|
|
8
|
+
* - ui.open(item) pre-fills from an existing row without an extra fetch
|
|
9
|
+
*
|
|
10
|
+
* Copied to the user's project alongside the generated connectors.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, computed } from 'vue';
|
|
13
|
+
import { mergeZodErrors } from './zod-error-merger.js';
|
|
14
|
+
import { getGlobalBaseUrl, mergeCallbacks } from '../composables/shared/runtime/apiHelpers.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param urlFn URL string or function that receives an id and returns the URL string.
|
|
18
|
+
* e.g. '/pet' (when ID is sent via body) or (id) => `/pet/${id}`
|
|
19
|
+
* @param options Configuration: schema, fields, method, baseURL, callbacks, etc.
|
|
20
|
+
*/
|
|
21
|
+
export function useUpdateConnector(urlFn, options = {}) {
|
|
22
|
+
const resolveUrl = (id) => (typeof urlFn === 'function' ? urlFn(id) : urlFn);
|
|
23
|
+
const {
|
|
24
|
+
schema: baseSchema,
|
|
25
|
+
schemaOverride,
|
|
26
|
+
fields = [],
|
|
27
|
+
method = 'PUT',
|
|
28
|
+
baseURL: baseURLOpt,
|
|
29
|
+
errorConfig = {},
|
|
30
|
+
onRequest: onRequestOpt,
|
|
31
|
+
onSuccess: onSuccessOpt,
|
|
32
|
+
onError: onErrorOpt,
|
|
33
|
+
onFinish: onFinishOpt,
|
|
34
|
+
autoClose = true,
|
|
35
|
+
skipGlobalCallbacks,
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
const baseURL = baseURLOpt || getGlobalBaseUrl();
|
|
39
|
+
if (!baseURL) {
|
|
40
|
+
console.warn('[useUpdateConnector] No baseURL configured. Set runtimeConfig.public.apiBaseUrl in nuxt.config.ts or pass baseURL in options.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve active schema: schemaOverride(base) / schemaOverride / base / none
|
|
44
|
+
const schema = schemaOverride
|
|
45
|
+
? typeof schemaOverride === 'function'
|
|
46
|
+
? schemaOverride(baseSchema)
|
|
47
|
+
: schemaOverride
|
|
48
|
+
: baseSchema;
|
|
49
|
+
|
|
50
|
+
if (schemaOverride && !schema) {
|
|
51
|
+
console.warn('[useUpdateConnector] schemaOverride resolved to undefined — validation will be skipped. Check your schemaOverride function returns a valid Zod schema.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Form state ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const model = ref({});
|
|
57
|
+
const errors = ref({});
|
|
58
|
+
const loading = ref(false);
|
|
59
|
+
const error = ref(null);
|
|
60
|
+
const submitted = ref(false);
|
|
61
|
+
const targetId = ref(null);
|
|
62
|
+
|
|
63
|
+
// Callbacks — developer-assignable (can also be passed as options)
|
|
64
|
+
// Both the connector-level option and the per-operation registration are called.
|
|
65
|
+
let _localOnSuccess = null;
|
|
66
|
+
let _localOnError = null;
|
|
67
|
+
|
|
68
|
+
// ── UI state ───────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const isOpen = ref(false);
|
|
71
|
+
|
|
72
|
+
const ui = {
|
|
73
|
+
isOpen,
|
|
74
|
+
/**
|
|
75
|
+
* Open the update form.
|
|
76
|
+
* @param item If provided, pre-fills the model immediately (no extra fetch needed).
|
|
77
|
+
* Typically pass the row object from the table.
|
|
78
|
+
*/
|
|
79
|
+
open(item) {
|
|
80
|
+
if (item) {
|
|
81
|
+
setValues(item);
|
|
82
|
+
}
|
|
83
|
+
isOpen.value = true;
|
|
84
|
+
},
|
|
85
|
+
close() {
|
|
86
|
+
isOpen.value = false;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ── Derived ────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const isValid = computed(() => {
|
|
93
|
+
if (!schema) return true;
|
|
94
|
+
return schema.safeParse(model.value).success;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const hasErrors = computed(() => Object.keys(errors.value).length > 0);
|
|
98
|
+
|
|
99
|
+
// ── Actions ────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function setValues(data) {
|
|
102
|
+
model.value = { ...model.value, ...data };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setField(key, value) {
|
|
106
|
+
model.value = { ...model.value, [key]: value };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reset() {
|
|
110
|
+
model.value = {};
|
|
111
|
+
errors.value = {};
|
|
112
|
+
error.value = null;
|
|
113
|
+
submitted.value = false;
|
|
114
|
+
targetId.value = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fetch the current item by ID and pre-fill the form model.
|
|
119
|
+
* Use this when you need fresh data from the server before editing.
|
|
120
|
+
* If the row data from the table is sufficient, use ui.open(item) instead.
|
|
121
|
+
*/
|
|
122
|
+
async function load(id) {
|
|
123
|
+
if (id === undefined || id === null) {
|
|
124
|
+
console.warn('[useUpdateConnector] load() called with undefined/null id — request was not sent.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
loading.value = true;
|
|
128
|
+
error.value = null;
|
|
129
|
+
|
|
130
|
+
const loadUrl = resolveUrl(id);
|
|
131
|
+
const mergedGet = mergeCallbacks(loadUrl, 'GET', {
|
|
132
|
+
onError: onErrorOpt,
|
|
133
|
+
}, skipGlobalCallbacks);
|
|
134
|
+
const loadMods = await mergedGet.onRequest({ url: loadUrl, method: 'GET' });
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const result = await $fetch(loadUrl, {
|
|
138
|
+
method: 'GET',
|
|
139
|
+
...(loadMods?.headers ? { headers: loadMods.headers } : {}),
|
|
140
|
+
...(loadMods?.query ? { query: loadMods.query } : {}),
|
|
141
|
+
...(baseURL ? { baseURL } : {}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
targetId.value = id;
|
|
145
|
+
setValues(result);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
error.value = err;
|
|
148
|
+
await mergedGet.onError(err, { operation: 'update' });
|
|
149
|
+
_localOnError?.(err);
|
|
150
|
+
throw err;
|
|
151
|
+
} finally {
|
|
152
|
+
loading.value = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validate with Zod (if schema provided) then PUT/PATCH via $fetch.
|
|
158
|
+
* @param id The resource ID to update.
|
|
159
|
+
* @param data Optional payload override. Falls back to model.value.
|
|
160
|
+
* @returns The response data, or undefined if validation failed.
|
|
161
|
+
*/
|
|
162
|
+
async function execute(id, data) {
|
|
163
|
+
submitted.value = true;
|
|
164
|
+
const payload = data ?? model.value;
|
|
165
|
+
|
|
166
|
+
// 1. Zod validation
|
|
167
|
+
if (schema) {
|
|
168
|
+
const result = schema.safeParse(payload);
|
|
169
|
+
if (!result.success) {
|
|
170
|
+
errors.value = mergeZodErrors(result.error.flatten().fieldErrors, errorConfig);
|
|
171
|
+
console.error('[useUpdateConnector] Validation failed — request was not sent.', errors.value);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
errors.value = {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. $fetch PUT/PATCH
|
|
178
|
+
if (id === undefined || id === null) {
|
|
179
|
+
console.warn('[useUpdateConnector] execute() called with undefined/null id — request was not sent.');
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
loading.value = true;
|
|
183
|
+
error.value = null;
|
|
184
|
+
|
|
185
|
+
const url = resolveUrl(id);
|
|
186
|
+
|
|
187
|
+
// Merge global + local callbacks (onRequest modifications, rule-based suppression)
|
|
188
|
+
const merged = mergeCallbacks(url, method, {
|
|
189
|
+
onRequest: onRequestOpt,
|
|
190
|
+
onSuccess: onSuccessOpt,
|
|
191
|
+
onError: onErrorOpt,
|
|
192
|
+
onFinish: onFinishOpt,
|
|
193
|
+
}, skipGlobalCallbacks);
|
|
194
|
+
|
|
195
|
+
// onRequest hook — collects header/body/query modifications from global rules and local option
|
|
196
|
+
const requestMods = await merged.onRequest({ url, method, body: payload });
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const result = await $fetch(url, {
|
|
200
|
+
method,
|
|
201
|
+
body: requestMods?.body ?? payload,
|
|
202
|
+
...(requestMods?.headers ? { headers: requestMods.headers } : {}),
|
|
203
|
+
...(requestMods?.query ? { query: requestMods.query } : {}),
|
|
204
|
+
...(baseURL ? { baseURL } : {}),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await merged.onSuccess(result, { operation: 'update' });
|
|
208
|
+
_localOnSuccess?.(result);
|
|
209
|
+
|
|
210
|
+
if (autoClose) ui.close();
|
|
211
|
+
|
|
212
|
+
await merged.onFinish({ url, method, data: result, success: true });
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
error.value = err;
|
|
217
|
+
await merged.onError(err, { operation: 'update' });
|
|
218
|
+
_localOnError?.(err);
|
|
219
|
+
|
|
220
|
+
await merged.onFinish({ url, method, error: err, success: false });
|
|
221
|
+
|
|
222
|
+
throw err;
|
|
223
|
+
} finally {
|
|
224
|
+
loading.value = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Return ─────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
// Form state
|
|
232
|
+
model,
|
|
233
|
+
errors,
|
|
234
|
+
loading,
|
|
235
|
+
error,
|
|
236
|
+
submitted,
|
|
237
|
+
isValid,
|
|
238
|
+
hasErrors,
|
|
239
|
+
fields: computed(() => fields),
|
|
240
|
+
targetId,
|
|
241
|
+
|
|
242
|
+
// Actions
|
|
243
|
+
load,
|
|
244
|
+
execute,
|
|
245
|
+
refresh: execute,
|
|
246
|
+
reset,
|
|
247
|
+
setValues,
|
|
248
|
+
setField,
|
|
249
|
+
|
|
250
|
+
// Callbacks
|
|
251
|
+
onSuccess: (fn) => { _localOnSuccess = fn; },
|
|
252
|
+
onError: (fn) => { _localOnError = fn; },
|
|
253
|
+
|
|
254
|
+
// UI
|
|
255
|
+
ui,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// @ts-nocheck - This file runs in user's Nuxt project with different TypeScript config
|
|
2
|
+
/**
|
|
3
|
+
* zod-error-merger — Merge Zod fieldErrors with per-field error message overrides.
|
|
4
|
+
*
|
|
5
|
+
* Priority (highest → lowest):
|
|
6
|
+
* 3. config.fields[fieldName].errors[zodCode] — per-field, per-code override
|
|
7
|
+
* 2. z.setErrorMap() — global translation (set by the developer)
|
|
8
|
+
* 1. Zod defaults — built-in English messages
|
|
9
|
+
*
|
|
10
|
+
* This function handles priority 3 only. Priority 2 is handled automatically by Zod
|
|
11
|
+
* when the developer sets z.setErrorMap() in their plugins/zod-i18n.ts.
|
|
12
|
+
*
|
|
13
|
+
* Copied to the user's project alongside the generated connectors.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map Zod issue codes to friendlier config key names.
|
|
18
|
+
* The developer uses these keys in config.fields[name].errors:
|
|
19
|
+
*
|
|
20
|
+
* errors: {
|
|
21
|
+
* required: 'Name is required',
|
|
22
|
+
* min: 'At least 1 character',
|
|
23
|
+
* max: 'Max 100 characters',
|
|
24
|
+
* email: 'Invalid email',
|
|
25
|
+
* enum: 'Select a valid option',
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
const ZOD_CODE_TO_CONFIG_KEY = {
|
|
29
|
+
too_small: 'min',
|
|
30
|
+
too_big: 'max',
|
|
31
|
+
invalid_type: 'required', // most common: field is undefined/null
|
|
32
|
+
invalid_enum_value: 'enum',
|
|
33
|
+
invalid_string: 'format',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert Zod's flatten().fieldErrors to Record<string, string>,
|
|
38
|
+
* merging optional per-field message overrides from the component config.
|
|
39
|
+
*
|
|
40
|
+
* @param fieldErrors Output of zodResult.error.flatten().fieldErrors
|
|
41
|
+
* Shape: { fieldName: string[] }
|
|
42
|
+
* @param errorConfig Optional per-field error config from index.ts
|
|
43
|
+
* Shape: { fieldName: { required?: string, min?: string, ... } }
|
|
44
|
+
*/
|
|
45
|
+
export function mergeZodErrors(fieldErrors, errorConfig = {}) {
|
|
46
|
+
const result = {};
|
|
47
|
+
|
|
48
|
+
for (const [field, messages] of Object.entries(fieldErrors)) {
|
|
49
|
+
if (!messages || messages.length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The first message is the most relevant one
|
|
54
|
+
const defaultMessage = messages[0];
|
|
55
|
+
|
|
56
|
+
// Check if there's a config override for this field
|
|
57
|
+
const fieldConfig = errorConfig[field];
|
|
58
|
+
if (!fieldConfig) {
|
|
59
|
+
result[field] = defaultMessage;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Try to map the Zod message to a config key.
|
|
64
|
+
// We check for simple substrings in the Zod message to identify the code.
|
|
65
|
+
const configMessage = resolveConfigMessage(defaultMessage, fieldConfig);
|
|
66
|
+
result[field] = configMessage ?? defaultMessage;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Try to find a matching override in fieldConfig based on the Zod message content.
|
|
74
|
+
* Returns the override string, or null if no match found.
|
|
75
|
+
*/
|
|
76
|
+
function resolveConfigMessage(zodMessage, fieldConfig) {
|
|
77
|
+
if (!fieldConfig || typeof fieldConfig !== 'object') {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Direct key match: developer can use zod code names directly
|
|
82
|
+
// e.g. errors.too_small, errors.invalid_type
|
|
83
|
+
for (const [zodCode, configKey] of Object.entries(ZOD_CODE_TO_CONFIG_KEY)) {
|
|
84
|
+
if (fieldConfig[zodCode]) {
|
|
85
|
+
// Check if Zod message suggests this error type
|
|
86
|
+
if (messageMatchesCode(zodMessage, zodCode)) {
|
|
87
|
+
return fieldConfig[zodCode];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Also support friendly key names: errors.min, errors.required, etc.
|
|
91
|
+
if (fieldConfig[configKey]) {
|
|
92
|
+
if (messageMatchesCode(zodMessage, zodCode)) {
|
|
93
|
+
return fieldConfig[configKey];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fallback: if developer set errors.required and Zod says "Required"
|
|
99
|
+
if (fieldConfig.required && /required|undefined|null/i.test(zodMessage)) {
|
|
100
|
+
return fieldConfig.required;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Heuristic: does the Zod default message suggest a certain error code?
|
|
108
|
+
*/
|
|
109
|
+
function messageMatchesCode(message, zodCode) {
|
|
110
|
+
const patterns = {
|
|
111
|
+
too_small: /at least|minimum|must contain at least|min/i,
|
|
112
|
+
too_big: /at most|maximum|must contain at most|max/i,
|
|
113
|
+
invalid_type: /required|expected|received undefined|null/i,
|
|
114
|
+
invalid_enum_value: /invalid enum|expected one of/i,
|
|
115
|
+
invalid_string: /invalid|email|url|uuid|datetime/i,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return patterns[zodCode]?.test(message) ?? false;
|
|
119
|
+
}
|