Skip to content

Commit b5b9efe

Browse files
ScriptTypeymc9
andauthored
Angular tanstack query (#2206)
Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com>
1 parent f889796 commit b5b9efe

File tree

7 files changed

+422
-3
lines changed

7 files changed

+422
-3
lines changed

packages/plugins/tanstack-query/package.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
"import": "./runtime-v5/svelte.mjs",
5959
"require": "./runtime-v5/svelte.js",
6060
"default": "./runtime-v5/svelte.js"
61+
},
62+
"./runtime-v5/angular": {
63+
"types": "./runtime-v5/angular.d.ts",
64+
"import": "./runtime-v5/angular.mjs",
65+
"require": "./runtime-v5/angular.js",
66+
"default": "./runtime-v5/angular.js"
6167
}
6268
},
6369
"repository": {
@@ -88,6 +94,10 @@
8894
"ts-pattern": "^4.3.0"
8995
},
9096
"devDependencies": {
97+
"@angular/core": "^20.0.0",
98+
"@angular/common": "^20.0.0",
99+
"@angular/platform-browser": "^20.0.0",
100+
"@tanstack/angular-query-v5": "npm:@tanstack/angular-query-experimental@5.84.x",
91101
"@tanstack/react-query": "^4.29.7",
92102
"@tanstack/react-query-v5": "npm:@tanstack/react-query@5.56.x",
93103
"@tanstack/svelte-query": "^4.29.7",
@@ -103,9 +113,12 @@
103113
"nock": "^13.3.6",
104114
"react": "18.2.0",
105115
"react-test-renderer": "^18.2.0",
116+
"rxjs": "^7.8.0",
106117
"svelte": "^4.2.1",
107118
"swr": "^2.0.3",
108119
"tmp": "^0.2.3",
109-
"vue": "^3.3.4"
120+
"typescript": "^5.0.0",
121+
"vue": "^3.3.4",
122+
"zone.js": "^0.15.0"
110123
}
111124
}

packages/plugins/tanstack-query/scripts/postbuild.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,10 @@ replaceSync({
4545
from: /@tanstack\/vue-query-v5/g,
4646
to: '@tanstack/vue-query',
4747
});
48+
49+
console.log('Replacing @tanstack/angular-query-v5');
50+
replaceSync({
51+
file: 'dist/runtime-v5/angular*(.d.ts|.d.mts|.js|.mjs)',
52+
from: /@tanstack\/angular-query-v5/g,
53+
to: '@tanstack/angular-query-experimental',
54+
});

packages/plugins/tanstack-query/src/generator.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
2121
import { P, match } from 'ts-pattern';
2222
import { name } from '.';
2323

24-
const supportedTargets = ['react', 'vue', 'svelte'];
24+
const supportedTargets = ['react', 'vue', 'svelte', 'angular'];
2525
type TargetFramework = (typeof supportedTargets)[number];
2626
type TanStackVersion = 'v4' | 'v5';
2727

@@ -41,6 +41,11 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
4141
throw new PluginError(name, `Unsupported version "${version}": use "v4" or "v5"`);
4242
}
4343

44+
// Angular is only supported in v5
45+
if (target === 'angular' && version !== 'v5') {
46+
throw new PluginError(name, `Angular target is only supported with version "v5", got "${version}"`);
47+
}
48+
4449
let outDir = requireOption<string>(options, 'output', name);
4550
outDir = resolvePath(outDir, options);
4651
ensureEmptyDir(outDir);
@@ -224,6 +229,7 @@ function generateMutationHook(
224229
switch (target) {
225230
case 'react':
226231
case 'vue':
232+
case 'angular':
227233
// override the mutateAsync function to return the correct type
228234
func.addVariableStatement({
229235
declarationKind: VariableDeclarationKind.Const,
@@ -597,6 +603,11 @@ function generateIndex(
597603
case 'svelte':
598604
sf.addStatements(`export { SvelteQueryContextKey, setHooksContext } from '${runtimeImportBase}/svelte';`);
599605
break;
606+
case 'angular':
607+
sf.addStatements(
608+
`export { AngularQueryContextKey, provideAngularQueryContext } from '${runtimeImportBase}/angular';`
609+
);
610+
break;
600611
}
601612
sf.addStatements(`export { default as metadata } from './__model_meta';`);
602613
}
@@ -609,6 +620,8 @@ function makeGetContext(target: TargetFramework) {
609620
return 'const { endpoint, fetch } = getHooksContext();';
610621
case 'svelte':
611622
return `const { endpoint, fetch } = getHooksContext();`;
623+
case 'angular':
624+
return 'const { endpoint, fetch } = getHooksContext();';
612625
default:
613626
throw new PluginError(name, `Unsupported target "${target}"`);
614627
}
@@ -658,6 +671,13 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) {
658671
...shared,
659672
];
660673
}
674+
case 'angular': {
675+
return [
676+
`import type { CreateMutationOptions, CreateQueryOptions, CreateInfiniteQueryOptions, InfiniteData } from '@tanstack/angular-query-v5';`,
677+
`import { getHooksContext } from '${runtimeImportBase}/${target}';`,
678+
...shared,
679+
];
680+
}
661681
default:
662682
throw new PluginError(name, `Unsupported target: ${target}`);
663683
}
@@ -707,6 +727,11 @@ function makeQueryOptions(
707727
? `Omit<CreateQueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>`
708728
: `StoreOrVal<Omit<CreateQueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>>`
709729
)
730+
.with('angular', () =>
731+
infinite
732+
? `Omit<CreateInfiniteQueryOptions<${returnType}, TError, InfiniteData<${dataType}>>, 'queryKey' | 'initialPageParam'>`
733+
: `Omit<CreateQueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>`
734+
)
710735
.otherwise(() => {
711736
throw new PluginError(name, `Unsupported target: ${target}`);
712737
});
@@ -727,6 +752,7 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin
727752
return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`;
728753
})
729754
.with('svelte', () => `MutationOptions<${returnType}, DefaultError, ${argsType}>`)
755+
.with('angular', () => `CreateMutationOptions<${returnType}, DefaultError, ${argsType}>`)
730756
.otherwise(() => {
731757
throw new PluginError(name, `Unsupported target: ${target}`);
732758
});
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import {
3+
injectQuery,
4+
injectMutation,
5+
injectInfiniteQuery,
6+
QueryClient,
7+
type CreateQueryOptions,
8+
type CreateMutationOptions,
9+
type CreateInfiniteQueryOptions,
10+
type InfiniteData,
11+
CreateInfiniteQueryResult,
12+
QueryKey,
13+
} from '@tanstack/angular-query-v5';
14+
import type { ModelMeta } from '@zenstackhq/runtime/cross';
15+
import { inject, InjectionToken } from '@angular/core';
16+
import {
17+
APIContext,
18+
DEFAULT_QUERY_ENDPOINT,
19+
fetcher,
20+
getQueryKey,
21+
makeUrl,
22+
marshal,
23+
setupInvalidation,
24+
setupOptimisticUpdate,
25+
type ExtraMutationOptions,
26+
type ExtraQueryOptions,
27+
type FetchFn,
28+
} from '../runtime/common';
29+
30+
export { APIContext as RequestHandlerContext } from '../runtime/common';
31+
32+
export const AngularQueryContextKey = new InjectionToken<APIContext>('zenstack-angular-query-context');
33+
34+
/**
35+
* Provide context for the generated TanStack Query hooks.
36+
*/
37+
export function provideAngularQueryContext(context: APIContext) {
38+
return {
39+
provide: AngularQueryContextKey,
40+
useValue: context,
41+
};
42+
}
43+
44+
/**
45+
* Hooks context.
46+
*/
47+
export function getHooksContext() {
48+
const context = inject(AngularQueryContextKey, {
49+
optional: true,
50+
}) || {
51+
endpoint: DEFAULT_QUERY_ENDPOINT,
52+
fetch: undefined,
53+
logging: false,
54+
};
55+
56+
const { endpoint, ...rest } = context;
57+
return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest };
58+
}
59+
60+
/**
61+
* Creates an Angular TanStack Query query.
62+
*
63+
* @param model The name of the model under query.
64+
* @param url The request URL.
65+
* @param args The request args object, URL-encoded and appended as "?q=" parameter
66+
* @param options The Angular query options object
67+
* @param fetch The fetch function to use for sending the HTTP request
68+
* @returns injectQuery hook
69+
*/
70+
export function useModelQuery<TQueryFnData, TData, TError>(
71+
model: string,
72+
url: string,
73+
args?: unknown,
74+
options?: Omit<CreateQueryOptions<TQueryFnData, TError, TData>, 'queryKey'> & ExtraQueryOptions,
75+
fetch?: FetchFn
76+
) {
77+
const reqUrl = makeUrl(url, args);
78+
const queryKey = getQueryKey(model, url, args, {
79+
infinite: false,
80+
optimisticUpdate: options?.optimisticUpdate !== false,
81+
});
82+
return {
83+
queryKey,
84+
...injectQuery(() => ({
85+
queryKey,
86+
queryFn: ({ signal }) => fetcher<TQueryFnData, false>(reqUrl, { signal }, fetch, false),
87+
...options,
88+
})),
89+
};
90+
}
91+
92+
/**
93+
* Creates an Angular TanStack Query infinite query.
94+
*
95+
* @param model The name of the model under query.
96+
* @param url The request URL.
97+
* @param args The initial request args object, URL-encoded and appended as "?q=" parameter
98+
* @param options The Angular infinite query options object
99+
* @param fetch The fetch function to use for sending the HTTP request
100+
* @returns injectInfiniteQuery hook
101+
*/
102+
export function useInfiniteModelQuery<TQueryFnData, TData, TError>(
103+
model: string,
104+
url: string,
105+
args: unknown,
106+
options: Omit<
107+
CreateInfiniteQueryOptions<TQueryFnData, TError, InfiniteData<TData>>,
108+
'queryKey' | 'initialPageParam'
109+
>,
110+
fetch?: FetchFn
111+
): CreateInfiniteQueryResult<InfiniteData<TData>, TError> & { queryKey: QueryKey } {
112+
const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false });
113+
return {
114+
queryKey,
115+
...injectInfiniteQuery(() => ({
116+
queryKey,
117+
queryFn: ({ pageParam, signal }) => {
118+
return fetcher<TQueryFnData, false>(makeUrl(url, pageParam ?? args), { signal }, fetch, false);
119+
},
120+
initialPageParam: args,
121+
...options,
122+
})),
123+
};
124+
}
125+
126+
/**
127+
* Creates an Angular TanStack Query mutation.
128+
*
129+
* @param model The name of the model under mutation.
130+
* @param method The HTTP method.
131+
* @param url The request URL.
132+
* @param modelMeta The model metadata.
133+
* @param options The Angular mutation options.
134+
* @param fetch The fetch function to use for sending the HTTP request
135+
* @param checkReadBack Whether to check for read back errors and return undefined if found.
136+
* @returns injectMutation hook
137+
*/
138+
export function useModelMutation<
139+
TArgs,
140+
TError,
141+
R = any,
142+
C extends boolean = boolean,
143+
Result = C extends true ? R | undefined : R
144+
>(
145+
model: string,
146+
method: 'POST' | 'PUT' | 'DELETE',
147+
url: string,
148+
modelMeta: ModelMeta,
149+
options?: Omit<CreateMutationOptions<Result, TError, TArgs>, 'mutationFn'> & ExtraMutationOptions,
150+
fetch?: FetchFn,
151+
checkReadBack?: C
152+
) {
153+
const queryClient = inject(QueryClient);
154+
const mutationFn = (data: unknown) => {
155+
const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url;
156+
const fetchInit: RequestInit = {
157+
method,
158+
...(method !== 'DELETE' && {
159+
headers: {
160+
'content-type': 'application/json',
161+
},
162+
body: marshal(data),
163+
}),
164+
};
165+
return fetcher<R, C>(reqUrl, fetchInit, fetch, checkReadBack) as Promise<Result>;
166+
};
167+
168+
const finalOptions = { ...options, mutationFn };
169+
const operation = url.split('/').pop();
170+
const invalidateQueries = options?.invalidateQueries !== false;
171+
const optimisticUpdate = !!options?.optimisticUpdate;
172+
173+
if (operation) {
174+
const { logging } = getHooksContext();
175+
if (invalidateQueries) {
176+
setupInvalidation(
177+
model,
178+
operation,
179+
modelMeta,
180+
finalOptions,
181+
(predicate) => queryClient.invalidateQueries({ predicate }),
182+
logging
183+
);
184+
}
185+
186+
if (optimisticUpdate) {
187+
setupOptimisticUpdate(
188+
model,
189+
operation,
190+
modelMeta,
191+
finalOptions,
192+
queryClient.getQueryCache().getAll(),
193+
(queryKey, data) => {
194+
// update query cache
195+
queryClient.setQueryData<unknown>(queryKey, data);
196+
// cancel on-flight queries to avoid redundant cache updates,
197+
// the settlement of the current mutation will trigger a new revalidation
198+
queryClient.cancelQueries({ queryKey }, { revert: false, silent: true });
199+
},
200+
invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined,
201+
logging
202+
);
203+
}
204+
}
205+
206+
return injectMutation(() => finalOptions);
207+
}

0 commit comments

Comments
 (0)