metaxy_cli/codegen/
vue.rs1use crate::model::Manifest;
2
3use super::common::{self, FrameworkConfig};
4
5const QUERY_OPTIONS_INTERFACE: &str = r#"export interface QueryOptions<K extends QueryKey> {
6 /**
7 * Whether to execute the query. @default true
8 *
9 * Pass a getter `() => bool` for reactive updates — a plain `boolean` is
10 * read once when `useQuery` is called and will not trigger re-fetches.
11 */
12 enabled?: boolean | (() => boolean);
13
14 /** Auto-refetch interval in milliseconds. Set to 0 or omit to disable. */
15 refetchInterval?: number;
16
17 /** Initial data shown before the first fetch completes. */
18 placeholderData?: QueryOutput<K>;
19
20 /** Per-call options forwarded to client.query(). */
21 callOptions?: CallOptions;
22
23 /** Called when the query succeeds. */
24 onSuccess?: (data: QueryOutput<K>) => void;
25
26 /** Called when the query fails. */
27 onError?: (error: RpcError) => void;
28
29 /** Called when the query settles (success or failure). */
30 onSettled?: () => void;
31}"#;
32
33const QUERY_RESULT_INTERFACE: &str = r#"export interface QueryResult<K extends QueryKey> {
34 /** The latest successfully resolved data, or placeholderData. */
35 readonly data: Ref<QueryOutput<K> | undefined>;
36
37 /** The error from the most recent failed fetch, cleared on next attempt. */
38 readonly error: Ref<RpcError | undefined>;
39
40 /** True while a fetch is in-flight (including the initial fetch). */
41 readonly isLoading: Ref<boolean>;
42
43 /** True after the first successful fetch. Stays true even if a later refetch fails. */
44 readonly isSuccess: ComputedRef<boolean>;
45
46 /** True when the most recent fetch failed. */
47 readonly isError: ComputedRef<boolean>;
48
49 /** Manually trigger a refetch. No-op when `enabled` is false. Resets the polling interval. */
50 refetch: () => Promise<void>;
51}"#;
52
53const MUTATION_OPTIONS_INTERFACE: &str = r#"export interface MutationOptions<K extends MutationKey> {
54 /** Per-call options forwarded to client.mutate(). */
55 callOptions?: CallOptions;
56
57 /** Called when the mutation succeeds. */
58 onSuccess?: (data: MutationOutput<K>) => void;
59
60 /** Called when the mutation fails. */
61 onError?: (error: RpcError) => void;
62
63 /** Called when the mutation settles (success or failure). */
64 onSettled?: () => void;
65}"#;
66
67const MUTATION_RESULT_INTERFACE: &str = r#"export interface MutationResult<K extends MutationKey> {
68 /** Execute the mutation. Rejects on error. */
69 mutate: (...args: MutationArgs<K>) => Promise<void>;
70
71 /** Execute the mutation and return the result. Rejects on error. */
72 mutateAsync: (...args: MutationArgs<K>) => Promise<MutationOutput<K>>;
73
74 /** The latest successfully resolved data. */
75 readonly data: Ref<MutationOutput<K> | undefined>;
76
77 /** The error from the most recent failed mutation, cleared on next attempt. */
78 readonly error: Ref<RpcError | undefined>;
79
80 /** True while a mutation is in-flight. */
81 readonly isLoading: Ref<boolean>;
82
83 /** True after the most recent mutation succeeded. */
84 readonly isSuccess: ComputedRef<boolean>;
85
86 /** True when the most recent mutation failed. */
87 readonly isError: ComputedRef<boolean>;
88
89 /** Reset state back to idle (clear data, error, status). */
90 reset: () => void;
91}"#;
92
93const USE_QUERY_IMPL: &str = r#"export function useQuery<K extends QueryKey>(
94 client: RpcClient,
95 ...args: unknown[]
96): QueryResult<K> {
97 const key = args[0] as K;
98
99 let inputFn: (() => QueryInput<K>) | undefined;
100 let optionsArg: QueryOptions<K> | (() => QueryOptions<K>) | undefined;
101
102 if (typeof args[1] === "function" && args[2] !== undefined) {
103 inputFn = args[1] as () => QueryInput<K>;
104 optionsArg = args[2] as QueryOptions<K> | (() => QueryOptions<K>) | undefined;
105 } else if (typeof args[1] === "function") {
106 if (VOID_QUERY_KEYS.has(key)) {
107 optionsArg = args[1] as () => QueryOptions<K>;
108 } else {
109 inputFn = args[1] as () => QueryInput<K>;
110 }
111 } else if (typeof args[1] === "object") {
112 optionsArg = args[1] as QueryOptions<K>;
113 }
114
115 function resolveOptions(): QueryOptions<K> | undefined {
116 return typeof optionsArg === "function" ? optionsArg() : optionsArg;
117 }
118
119 function resolveEnabled(): boolean {
120 const opts = resolveOptions();
121 return typeof opts?.enabled === "function"
122 ? opts.enabled()
123 : (opts?.enabled ?? true);
124 }
125
126 const data = ref<QueryOutput<K> | undefined>(resolveOptions()?.placeholderData) as Ref<QueryOutput<K> | undefined>;
127 const error = ref<RpcError | undefined>();
128 const hasFetched = ref(false);
129 const isLoading = ref(false);
130 const isSuccess = computed(() => hasFetched.value);
131 const isError = computed(() => error.value !== undefined);
132
133 let generation = 0;
134 let controller: AbortController | undefined;
135 let intervalId: ReturnType<typeof setInterval> | undefined;
136
137 async function fetchData(input: QueryInput<K> | undefined, signal: AbortSignal, gen: number) {
138 const opts = resolveOptions();
139 isLoading.value = true;
140 error.value = undefined;
141 try {
142 const callArgs: unknown[] = [key];
143 if (input !== undefined) callArgs.push(input);
144 const mergedCallOptions = { ...opts?.callOptions, signal: opts?.callOptions?.signal
145 ? AbortSignal.any([signal, opts.callOptions.signal])
146 : signal };
147 callArgs.push(mergedCallOptions);
148 const result = await (client.query as (...a: unknown[]) => Promise<unknown>)(
149 ...callArgs
150 ) as QueryOutput<K>;
151 if (gen !== generation) return;
152 data.value = result;
153 hasFetched.value = true;
154 opts?.onSuccess?.(data.value!);
155 } catch (e) {
156 if (gen !== generation) return;
157 error.value = e as RpcError;
158 opts?.onError?.(error.value);
159 } finally {
160 if (gen === generation) {
161 isLoading.value = false;
162 opts?.onSettled?.();
163 }
164 }
165 }
166
167 function setupInterval(enabled: boolean, refetchInterval: number | undefined) {
168 if (intervalId) { clearInterval(intervalId); intervalId = undefined; }
169 if (enabled && refetchInterval) {
170 intervalId = setInterval(() => {
171 if (controller && !controller.signal.aborted) {
172 void fetchData(inputFn?.(), controller.signal, generation);
173 }
174 }, refetchInterval);
175 }
176 }
177
178 const stopWatch = watch(
179 () => {
180 const enabled = resolveEnabled();
181 const input = inputFn?.();
182 return { enabled, input, serialized: JSON.stringify(input), refetchInterval: resolveOptions()?.refetchInterval };
183 },
184 (curr, prev) => {
185 const inputChanged = !prev || curr.enabled !== prev.enabled || curr.serialized !== prev.serialized;
186
187 if (inputChanged) {
188 if (controller) { controller.abort(); controller = undefined; }
189 if (curr.enabled) {
190 generation++;
191 const gen = generation;
192 controller = new AbortController();
193 void fetchData(curr.input, controller.signal, gen);
194 } else {
195 isLoading.value = false;
196 }
197 }
198
199 setupInterval(curr.enabled, curr.refetchInterval);
200 },
201 { immediate: true },
202 );
203
204 onScopeDispose(() => {
205 stopWatch();
206 generation++;
207 if (controller) controller.abort();
208 if (intervalId) clearInterval(intervalId);
209 });
210
211 return {
212 data,
213 error,
214 isLoading,
215 isSuccess,
216 isError,
217 refetch: async () => {
218 const enabled = resolveEnabled();
219 if (!enabled) return;
220 generation++;
221 const gen = generation;
222 const localController = new AbortController();
223 if (controller) controller.abort();
224 controller = localController;
225 setupInterval(enabled, resolveOptions()?.refetchInterval);
226 await fetchData(inputFn?.(), localController.signal, gen);
227 },
228 };
229}"#;
230
231const USE_MUTATION_IMPL: &str = r#"export function useMutation<K extends MutationKey>(
232 client: RpcClient,
233 key: K,
234 options?: MutationOptions<K>,
235): MutationResult<K> {
236 const data = ref<MutationOutput<K> | undefined>() as Ref<MutationOutput<K> | undefined>;
237 const error = ref<RpcError | undefined>();
238 const isLoading = ref(false);
239 const hasSucceeded = ref(false);
240 const isSuccess = computed(() => hasSucceeded.value);
241 const isError = computed(() => error.value !== undefined);
242
243 async function execute(...input: MutationArgs<K>): Promise<MutationOutput<K>> {
244 isLoading.value = true;
245 error.value = undefined;
246 hasSucceeded.value = false;
247 try {
248 const callArgs: unknown[] = [key];
249 if (input.length > 0) callArgs.push(input[0]);
250 if (options?.callOptions) callArgs.push(options.callOptions);
251 const result = await (client.mutate as (...a: unknown[]) => Promise<unknown>)(
252 ...callArgs
253 ) as MutationOutput<K>;
254 data.value = result;
255 hasSucceeded.value = true;
256 options?.onSuccess?.(result);
257 return result;
258 } catch (e) {
259 error.value = e as RpcError;
260 options?.onError?.(error.value);
261 throw e;
262 } finally {
263 isLoading.value = false;
264 options?.onSettled?.();
265 }
266 }
267
268 return {
269 mutate: async (...args: MutationArgs<K>) => { await execute(...args); },
270 mutateAsync: (...args: MutationArgs<K>) => execute(...args),
271 data,
272 error,
273 isLoading,
274 isSuccess,
275 isError,
276 reset: () => { data.value = undefined; error.value = undefined; isLoading.value = false; hasSucceeded.value = false; },
277 };
278}"#;
279
280const FRAMEWORK_IMPORT: &str =
281 "import { ref, computed, watch, onScopeDispose, type Ref, type ComputedRef } from \"vue\";";
282
283pub fn generate_vue_file(
288 manifest: &Manifest,
289 client_import_path: &str,
290 types_import_path: &str,
291 preserve_docs: bool,
292) -> String {
293 common::generate_framework_file(
294 manifest,
295 client_import_path,
296 types_import_path,
297 preserve_docs,
298 &FrameworkConfig {
299 framework_import: Some(FRAMEWORK_IMPORT),
300 query_fn_name: "useQuery",
301 input_as_getter: true,
302 query_interfaces: &[QUERY_OPTIONS_INTERFACE, QUERY_RESULT_INTERFACE],
303 mutation_interfaces: &[MUTATION_OPTIONS_INTERFACE, MUTATION_RESULT_INTERFACE],
304 query_impl: USE_QUERY_IMPL,
305 mutation_impl: USE_MUTATION_IMPL,
306 },
307 )
308}