diff --git a/.gitignore b/.gitignore index 322351819..cf9faf0b9 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,4 @@ typings/ package-lock.json tests/generated/** .turbo -.next +.next \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 3c0fa8a9d..e6baee546 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,8 @@ yarn.lock # Angular build cache **/.angular/cache +*.svg +*.ico +*.lock +.gitignore +.prettierignore \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 23300e28e..e177564f5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -262,6 +262,7 @@ export type InputOptions = { export const OutputClient = { ANGULAR: 'angular', + ANGULAR_QUERY: 'angular-query', AXIOS: 'axios', AXIOS_FUNCTIONS: 'axios-functions', REACT_QUERY: 'react-query', diff --git a/packages/orval/src/client.ts b/packages/orval/src/client.ts index e92154c81..e95559800 100644 --- a/packages/orval/src/client.ts +++ b/packages/orval/src/client.ts @@ -39,6 +39,7 @@ const getGeneratorClient = ( axios: axios({ type: 'axios' })(), 'axios-functions': axios({ type: 'axios-functions' })(), angular: angular()(), + 'angular-query': query({ output, type: 'angular-query' })(), 'react-query': query({ output, type: 'react-query' })(), 'svelte-query': query({ output, type: 'svelte-query' })(), 'vue-query': query({ output, type: 'vue-query' })(), diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index a638a5ffb..0cf0f8e79 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -46,11 +46,37 @@ import { } from './client'; import { getHasSignal, + getQueryTypeForFramework, + isAngular, isVue, normalizeQueryOptions, vueUnRefParams, vueWrapTypeWithMaybeRef, } from './utils'; +/** + * Get framework-aware prefix for hook names and type definitions + * @param hasSvelteQueryV4 - Whether using Svelte Query v4 + * @param isAngularClient - Whether using Angular client + * @param capitalize - Whether to capitalize the prefix (for type definitions) + * @returns The appropriate prefix string + */ +const getFrameworkPrefix = ( + hasSvelteQueryV4: boolean, + isAngularClient: boolean, + capitalize: boolean = false, +): string => { + let prefix: string; + + if (hasSvelteQueryV4) { + prefix = 'create'; + } else if (isAngularClient) { + prefix = 'inject'; + } else { + prefix = 'use'; + } + + return capitalize ? prefix.charAt(0).toUpperCase() + prefix.slice(1) : prefix; +}; const REACT_DEPENDENCIES: GeneratorDependency[] = [ { @@ -310,6 +336,39 @@ const VUE_QUERY_DEPENDENCIES: GeneratorDependency[] = [ }, ]; +const ANGULAR_QUERY_DEPENDENCIES: GeneratorDependency[] = [ + { + exports: [ + { name: 'injectQuery', values: true }, + { name: 'injectInfiniteQuery', values: true }, + { name: 'injectMutation', values: true }, + { name: 'InjectQueryOptions' }, + { name: 'InjectMutationOptions' }, + { name: 'CreateQueryOptions' }, + { name: 'CreateInfiniteQueryOptions' }, + { name: 'CreateMutationOptions' }, + { name: 'QueryFunction' }, + { name: 'MutationFunction' }, + { name: 'QueryKey' }, + { name: 'CreateQueryResult' }, + { name: 'CreateInfiniteQueryResult' }, + { name: 'InfiniteData' }, + { name: 'CreateMutationResult' }, + { name: 'DataTag' }, + { name: 'QueryClient' }, + ], + dependency: '@tanstack/angular-query-experimental', + }, + { + exports: [ + { name: 'inject', values: true }, + { name: 'Signal' }, + { name: 'computed', values: true }, + ], + dependency: '@angular/core', + }, +]; + const isVueQueryV3 = (packageJson: PackageJson | undefined) => { const hasVueQuery = packageJson?.dependencies?.['vue-query'] ?? @@ -340,10 +399,30 @@ export const getVueQueryDependencies: ClientDependenciesBuilder = ( ]; }; +export const getAngularQueryDependencies: ClientDependenciesBuilder = ( + hasGlobalMutator: boolean, + hasParamsSerializerOptions: boolean, + packageJson, + httpClient?: OutputHttpClient, +) => { + return [ + ...(!hasGlobalMutator && httpClient === OutputHttpClient.AXIOS + ? AXIOS_DEPENDENCIES + : []), + ...(hasParamsSerializerOptions ? PARAMS_SERIALIZER_DEPENDENCIES : []), + ...ANGULAR_QUERY_DEPENDENCIES, + ]; +}; + const isQueryV5 = ( packageJson: PackageJson | undefined, - queryClient: 'react-query' | 'vue-query' | 'svelte-query', + queryClient: 'react-query' | 'vue-query' | 'svelte-query' | 'angular-query', ) => { + // Angular Query is v5 only + if (queryClient === 'angular-query') { + return true; + } + const version = getPackageByQueryClient(packageJson, queryClient); if (!version) { @@ -357,8 +436,13 @@ const isQueryV5 = ( const isQueryV5WithDataTagError = ( packageJson: PackageJson | undefined, - queryClient: 'react-query' | 'vue-query' | 'svelte-query', + queryClient: 'react-query' | 'vue-query' | 'svelte-query' | 'angular-query', ) => { + // Angular Query is v5 only and supports DataTag + if (queryClient === 'angular-query') { + return true; + } + const version = getPackageByQueryClient(packageJson, queryClient); if (!version) { @@ -372,8 +456,13 @@ const isQueryV5WithDataTagError = ( const isQueryV5WithInfiniteQueryOptionsError = ( packageJson: PackageJson | undefined, - queryClient: 'react-query' | 'vue-query' | 'svelte-query', + queryClient: 'react-query' | 'vue-query' | 'svelte-query' | 'angular-query', ) => { + // Angular Query is v5 only and supports infinite query options + if (queryClient === 'angular-query') { + return true; + } + const version = getPackageByQueryClient(packageJson, queryClient); if (!version) { @@ -387,7 +476,7 @@ const isQueryV5WithInfiniteQueryOptionsError = ( const getPackageByQueryClient = ( packageJson: PackageJson | undefined, - queryClient: 'react-query' | 'vue-query' | 'svelte-query', + queryClient: 'react-query' | 'vue-query' | 'svelte-query' | 'angular-query', ) => { switch (queryClient) { case 'react-query': { @@ -411,6 +500,15 @@ const getPackageByQueryClient = ( packageJson?.peerDependencies?.['@tanstack/vue-query'] ); } + case 'angular-query': { + return ( + packageJson?.dependencies?.['@tanstack/angular-query-experimental'] ?? + packageJson?.devDependencies?.[ + '@tanstack/angular-query-experimental' + ] ?? + packageJson?.peerDependencies?.['@tanstack/angular-query-experimental'] + ); + } } }; @@ -487,6 +585,7 @@ const getQueryOptionsDefinition = ({ queryParam, isReturnType, initialData, + isAngularClient, }: { operationName: string; mutator?: GeneratorMutator; @@ -499,9 +598,10 @@ const getQueryOptionsDefinition = ({ queryParam?: string; isReturnType: boolean; initialData?: 'defined' | 'undefined'; + isAngularClient: boolean; }) => { const isMutatorHook = mutator?.isHook; - const prefix = !hasSvelteQueryV4 ? 'Use' : 'Create'; + const prefix = !hasSvelteQueryV4 && !isAngularClient ? 'Use' : 'Create'; const partialOptions = !isReturnType && hasQueryV5; if (type) { @@ -564,6 +664,7 @@ const generateQueryArguments = ({ queryParam, initialData, httpClient, + isAngularClient, }: { operationName: string; definitions: string; @@ -577,6 +678,7 @@ const generateQueryArguments = ({ queryParam?: string; initialData?: 'defined' | 'undefined'; httpClient: OutputHttpClient; + isAngularClient: boolean; }) => { const definition = getQueryOptionsDefinition({ operationName, @@ -590,6 +692,7 @@ const generateQueryArguments = ({ queryParam, isReturnType: false, initialData, + isAngularClient, }); if (!isRequestOptions) { @@ -628,6 +731,12 @@ const generateQueryReturnType = ({ isInitialDataDefined?: boolean; }) => { switch (outputClient) { + case OutputClient.ANGULAR_QUERY: { + if (type !== QueryType.INFINITE && type !== QueryType.SUSPENSE_INFINITE) { + return `CreateQueryResult`; + } + return `CreateInfiniteQueryResult`; + } case OutputClient.SVELTE_QUERY: { if (!hasSvelteQueryV4) { return `Use${pascal(type)}StoreResult { + if (outputClient === OutputClient.ANGULAR_QUERY) { + return `: CreateMutationResult< + Awaited>, + TError, + ${variableType}, + TContext + >`; + } if (outputClient === OutputClient.REACT_QUERY) { return `: UseMutationResult< Awaited>, @@ -878,6 +995,7 @@ const generateQueryImplementation = ({ queryParam, initialData: 'defined', httpClient, + isAngularClient: isAngular(outputClient), }); const undefinedInitialDataQueryArguments = generateQueryArguments({ operationName, @@ -892,6 +1010,7 @@ const generateQueryImplementation = ({ queryParam, initialData: 'undefined', httpClient, + isAngularClient: isAngular(outputClient), }); const queryArguments = generateQueryArguments({ operationName, @@ -905,6 +1024,7 @@ const generateQueryImplementation = ({ queryParams, queryParam, httpClient, + isAngularClient: isAngular(outputClient), }); const queryOptions = getQueryOptions({ @@ -938,6 +1058,7 @@ const generateQueryImplementation = ({ queryParams, queryParam, isReturnType: true, + isAngularClient: isAngular(outputClient), }); const queryOptionsImp = generateQueryOptions({ @@ -1031,16 +1152,17 @@ ${hookOptions} ? `{ queryKey, queryFn, ${queryOptionsImp}}` : 'customOptions' } as ${queryOptionFnReturnType} ${ - isVue(outputClient) + isVue(outputClient) || isAngular(outputClient) ? '' : `& { queryKey: ${hasQueryV5 ? `DataTag` : 'QueryKey'} }` } }`; - - const operationPrefix = hasSvelteQueryV4 ? 'create' : 'use'; - const optionalQueryClientArgument = hasQueryV5 - ? ', queryClient?: QueryClient' - : ''; + const operationPrefix = getFrameworkPrefix( + hasSvelteQueryV4, + isAngular(outputClient), + ); + const optionalQueryClientArgument = + hasQueryV5 && !isAngular(outputClient) ? ', queryClient?: QueryClient' : ''; const queryHookName = camel(`${operationPrefix}-${name}`); @@ -1079,12 +1201,18 @@ export function ${queryHookName}(\n ${q }${isRequestOptions ? 'options' : 'queryOptions'}) const ${queryResultVarName} = ${camel( - `${operationPrefix}-${type}`, - )}(${queryOptionsVarName} ${optionalQueryClientArgument ? ', queryClient' : ''}) as ${returnType}; + `${operationPrefix}-${isAngular(outputClient) || hasSvelteQueryV4 ? getQueryTypeForFramework(type) : type}`, + )}(${isAngular(outputClient) ? '() => ' : ''}${queryOptionsVarName}${!isAngular(outputClient) && optionalQueryClientArgument ? ', queryClient' : ''}) as ${returnType}; - ${queryResultVarName}.queryKey = ${ - isVue(outputClient) ? `unref(${queryOptionsVarName})` : queryOptionsVarName - }.queryKey ${isVue(outputClient) ? `as ${hasQueryV5 ? `DataTag` : 'QueryKey'}` : ''}; + ${ + isAngular(outputClient) + ? `` + : `${queryResultVarName}.queryKey = ${ + isVue(outputClient) + ? `unref(${queryOptionsVarName})` + : queryOptionsVarName + }.queryKey ${isVue(outputClient) ? `as ${hasQueryV5 ? `DataTag` : 'QueryKey'}` : ''};` + } return ${queryResultVarName}; }\n @@ -1145,21 +1273,33 @@ const generateQueryHook = async ( queryVersion === 5 || isQueryV5( context.output.packageJson, - outputClient as 'react-query' | 'vue-query' | 'svelte-query', + outputClient as + | 'react-query' + | 'vue-query' + | 'svelte-query' + | 'angular-query', ); const hasQueryV5WithDataTagError = queryVersion === 5 || isQueryV5WithDataTagError( context.output.packageJson, - outputClient as 'react-query' | 'vue-query' | 'svelte-query', + outputClient as + | 'react-query' + | 'vue-query' + | 'svelte-query' + | 'angular-query', ); const hasQueryV5WithInfiniteQueryOptionsError = queryVersion === 5 || isQueryV5WithInfiniteQueryOptionsError( context.output.packageJson, - outputClient as 'react-query' | 'vue-query' | 'svelte-query', + outputClient as + | 'react-query' + | 'vue-query' + | 'svelte-query' + | 'angular-query', ); const httpClient = context.output.httpClient; @@ -1427,6 +1567,7 @@ const generateQueryHook = async ( hasQueryV5, hasQueryV5WithInfiniteQueryOptionsError, isReturnType: true, + isAngularClient: isAngular(outputClient), }); const mutationArguments = generateQueryArguments({ @@ -1438,6 +1579,7 @@ const generateQueryHook = async ( hasQueryV5, hasQueryV5WithInfiniteQueryOptionsError, httpClient, + isAngularClient: isAngular(outputClient), }); const mutationOptionsFnName = camel( @@ -1502,8 +1644,8 @@ ${hooksOptionImplementation} : 'customOptions' }}`; - const operationPrefix = hasSvelteQueryV4 ? 'create' : 'use'; - const optionalQueryClientArgument = hasQueryV5 + const operationPrefix = getFrameworkPrefix(hasSvelteQueryV4, isAngular(outputClient)); + const optionalQueryClientArgument = hasQueryV5 && !isAngular(outputClient) ? ', queryClient?: QueryClient' : ''; @@ -1539,8 +1681,10 @@ ${mutationOptionsFn} isRequestOptions ? 'options' : 'mutationOptions' }); - return ${operationPrefix}Mutation(${mutationOptionsVarName} ${ - optionalQueryClientArgument ? ', queryClient' : '' + return ${operationPrefix}Mutation(${isAngular(outputClient) ? '() => ' : ''}${mutationOptionsVarName}${ + !isAngular(outputClient) && optionalQueryClientArgument + ? ', queryClient' + : '' }); } `; @@ -1594,12 +1738,13 @@ export const generateQuery: ClientBuilder = async ( }; const dependenciesBuilder: Record< - 'react-query' | 'vue-query' | 'svelte-query', + 'react-query' | 'vue-query' | 'svelte-query' | 'angular-query', ClientDependenciesBuilder > = { 'react-query': getReactQueryDependencies, 'vue-query': getVueQueryDependencies, 'svelte-query': getSvelteQueryDependencies, + 'angular-query': getAngularQueryDependencies, }; export const builder = @@ -1608,7 +1753,7 @@ export const builder = options: queryOptions, output, }: { - type?: 'react-query' | 'vue-query' | 'svelte-query'; + type?: 'react-query' | 'vue-query' | 'svelte-query' | 'angular-query'; options?: QueryOptions; output?: NormalizedOutputOptions; } = {}) => diff --git a/packages/query/src/utils.ts b/packages/query/src/utils.ts index 66798b319..bc2b9a17c 100644 --- a/packages/query/src/utils.ts +++ b/packages/query/src/utils.ts @@ -134,6 +134,21 @@ export const makeRouteSafe = (route: string): string => export const isVue = (client: OutputClient | OutputClientFunc) => OutputClient.VUE_QUERY === client; +export const isAngular = (client: OutputClient | OutputClientFunc) => + OutputClient.ANGULAR_QUERY === client; + +export const getQueryTypeForFramework = (type: string): string => { + // Angular Query and Svelte Query don't have suspense variants, map them to regular queries + switch (type) { + case 'suspenseQuery': + return 'query'; + case 'suspenseInfiniteQuery': + return 'infiniteQuery'; + default: + return type; + } +}; + export const getHasSignal = ({ overrideQuerySignal = false, verb, diff --git a/samples/angular-query/.gitignore b/samples/angular-query/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/samples/angular-query/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/samples/angular-query/angular.json b/samples/angular-query/angular.json new file mode 100644 index 000000000..77e5535b2 --- /dev/null +++ b/samples/angular-query/angular.json @@ -0,0 +1,108 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "yarn", + "analytics": false + }, + "newProjectRoot": "projects", + "projects": { + "angular-query": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-query:build:production" + }, + "development": { + "buildTarget": "angular-query:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n" + }, + "test": { + "builder": "@angular/build:unit-test", + "options": { + "tsConfig": "tsconfig.spec.json", + "runner": "vitest", + "buildTarget": "::development", + "setupFiles": ["vitest.setup.ts"] + } + } + } + } + } +} diff --git a/samples/angular-query/orval.config.ts b/samples/angular-query/orval.config.ts new file mode 100644 index 000000000..494f0bf74 --- /dev/null +++ b/samples/angular-query/orval.config.ts @@ -0,0 +1,58 @@ +import { faker } from '@faker-js/faker'; +import { defineConfig } from 'orval'; + +export default defineConfig({ + petstore: { + output: { + mode: 'tags-split', + target: 'src/api/endpoints/petstoreFromFileSpecWithTransformer.ts', + schemas: 'src/api/model', + client: 'angular-query', + mock: true, + tsconfig: './tsconfig.app.json', + clean: true, + override: { + mutator: { + path: 'src/api/mutator/custom-instance.ts', + }, + paramsSerializer: { + path: 'src/api/mutator/custom-params-serializer.ts', + }, + operations: { + listPets: { + mock: { + properties: () => { + return { + id: () => faker.number.int({ min: 1, max: 99999 }), + }; + }, + }, + }, + showPetById: { + mock: { + data: () => ({ + id: faker.number.int({ min: 1, max: 99 }), + name: faker.person.firstName(), + tag: faker.helpers.arrayElement([ + faker.word.sample(), + undefined, + ]), + }), + }, + }, + }, + mock: { + properties: { + '/tag|name/': () => faker.person.lastName(), + }, + }, + }, + }, + input: { + target: './petstore.yaml', + override: { + transformer: 'src/api/transformer/add-version.js', + }, + }, + }, +}); diff --git a/samples/angular-query/package.json b/samples/angular-query/package.json new file mode 100644 index 000000000..50a2dc428 --- /dev/null +++ b/samples/angular-query/package.json @@ -0,0 +1,42 @@ +{ + "name": "angular-query", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "CI=true ng test --no-watch", + "lint": "ng lint", + "generate-api": "node ../../packages/orval/dist/bin/orval.js" + }, + "private": true, + "dependencies": { + "@angular/animations": "^20.1.4", + "@angular/common": "^20.1.0", + "@angular/compiler": "^20.1.0", + "@angular/core": "^20.1.0", + "@angular/forms": "^20.1.0", + "@angular/platform-browser": "^20.1.0", + "@angular/router": "^20.1.0", + "@tanstack/angular-query-experimental": "^5.62.7", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^20.1.3", + "@angular/cli": "^20.1.3", + "@angular/compiler-cli": "^20.1.0", + "@faker-js/faker": "^9.9.0", + "@testing-library/angular": "16.0.0", + "msw": "^2.10.4", + "orval": "link:../../packages/orval/dist", + "typescript": "~5.8.2", + "vitest": "^3.2.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/samples/angular-query/petstore.yaml b/samples/angular-query/petstore.yaml new file mode 100644 index 000000000..cff71520e --- /dev/null +++ b/samples/angular-query/petstore.yaml @@ -0,0 +1,279 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /search: + get: + summary: search by query params + operationId: searchPets + tags: + - pets + parameters: + - name: requirednullableString + in: query + required: true + schema: + type: string + nullable: true + - name: requirednullableStringTwo + in: query + required: true + schema: + type: string + nullable: true + - name: nonRequirednullableString + in: query + required: false + schema: + type: string + nullable: true + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: string + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + application/xml: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - 'name' + - 'tag' + properties: + name: + type: string + tag: + type: string + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + - name: testId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{petId}/text: + get: + summary: Info for a specific pet + operationId: showPetText + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + text/plain: + schema: + type: string + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pet/{petId}/uploadImage: + post: + tags: + - pets + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: successful operation + '400': + description: No file uploaded + '404': + description: Pet not found + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pet/{petId}/downloadImage: + get: + tags: + - pets + summary: Download an image. + description: Download image of the pet. + operationId: downloadFile + parameters: + - name: petId + in: path + description: ID of pet to download + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: No file downloaded + '404': + description: Pet not found + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + type: object + required: + - id + - name + - requiredNullableString + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + requiredNullableString: + type: string + nullable: true + optionalNullableString: + type: string + nullable: true + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/samples/angular-query/public/logo.svg b/samples/angular-query/public/logo.svg new file mode 100644 index 000000000..ce80487cc --- /dev/null +++ b/samples/angular-query/public/logo.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/angular-query/public/mockServiceWorker.js b/samples/angular-query/public/mockServiceWorker.js new file mode 100644 index 000000000..be4527c7e --- /dev/null +++ b/samples/angular-query/public/mockServiceWorker.js @@ -0,0 +1,344 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.10.4' +const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + */ +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @returns {Promise} + */ +async function getResponse(event, client, requestId) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/samples/angular-query/src/api/browser.ts b/samples/angular-query/src/api/browser.ts new file mode 100644 index 000000000..c4b3ee6ac --- /dev/null +++ b/samples/angular-query/src/api/browser.ts @@ -0,0 +1,6 @@ +import { setupWorker } from 'msw/browser'; +import { getPetsMock } from './endpoints/pets/pets.msw'; + +const worker = setupWorker(...getPetsMock()); + +export { worker }; diff --git a/samples/angular-query/src/api/endpoints/pets/pets.msw.ts b/samples/angular-query/src/api/endpoints/pets/pets.msw.ts new file mode 100644 index 000000000..1c02d6fb9 --- /dev/null +++ b/samples/angular-query/src/api/endpoints/pets/pets.msw.ts @@ -0,0 +1,128 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ +import { + faker +} from '@faker-js/faker'; + +import { + HttpResponse, + delay, + http +} from 'msw'; + +import type { + Pet, + Pets +} from '../../model'; + + +export const getSearchPetsResponseMock = (): Pets => (Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({id: faker.number.int({min: undefined, max: undefined, multipleOf: undefined}), name: (() => faker.person.lastName())(), tag: (() => faker.person.lastName())(), requiredNullableString: faker.helpers.arrayElement([faker.string.alpha({length: {min: 10, max: 20}}), null]), optionalNullableString: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.string.alpha({length: {min: 10, max: 20}}), null]), undefined])}))) + +export const getListPetsResponseMock = (): Pets => (Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({id: faker.number.int({min: undefined, max: undefined, multipleOf: undefined}), name: (() => faker.person.lastName())(), tag: (() => faker.person.lastName())(), requiredNullableString: faker.helpers.arrayElement([faker.string.alpha({length: {min: 10, max: 20}}), null]), optionalNullableString: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.string.alpha({length: {min: 10, max: 20}}), null]), undefined])}))) + +export const getShowPetByIdResponseMock = () => ((() => ({ + id: faker.number.int({ min: 1, max: 99 }), + name: faker.person.firstName(), + tag: faker.helpers.arrayElement([ + faker.word.sample(), + void 0 + ]) + }))()) + +export const getShowPetTextResponseMock = (): string => (faker.word.sample()) + +export const getDownloadFileResponseMock = (): Blob => (new Blob(faker.helpers.arrayElements(faker.word.words(10).split(' ')))) + + +export const getSearchPetsMockHandler = (overrideResponse?: Pets | ((info: Parameters[1]>[0]) => Promise | Pets)) => { + return http.get('*/v:version/search', async (info) => {await delay(1000); + + return new HttpResponse(JSON.stringify(overrideResponse !== undefined + ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) + : getSearchPetsResponseMock()), + { status: 200, + headers: { 'Content-Type': 'application/json' } + }) + }) +} + +export const getListPetsMockHandler = (overrideResponse?: Pets | ((info: Parameters[1]>[0]) => Promise | Pets)) => { + return http.get('*/v:version/pets', async (info) => {await delay(1000); + + return new HttpResponse(JSON.stringify(overrideResponse !== undefined + ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) + : getListPetsResponseMock()), + { status: 200, + headers: { 'Content-Type': 'application/json' } + }) + }) +} + +export const getCreatePetsMockHandler = (overrideResponse?: null | ((info: Parameters[1]>[0]) => Promise | null)) => { + return http.post('*/v:version/pets', async (info) => {await delay(1000); + if (typeof overrideResponse === 'function') {await overrideResponse(info); } + return new HttpResponse(null, + { status: 201, + + }) + }) +} + +export const getShowPetByIdMockHandler = (overrideResponse?: Pet | ((info: Parameters[1]>[0]) => Promise | Pet)) => { + return http.get('*/v:version/pets/:petId', async (info) => {await delay(1000); + + return new HttpResponse(JSON.stringify(overrideResponse !== undefined + ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) + : getShowPetByIdResponseMock()), + { status: 200, + headers: { 'Content-Type': 'application/json' } + }) + }) +} + +export const getShowPetTextMockHandler = (overrideResponse?: string | ((info: Parameters[1]>[0]) => Promise | string)) => { + return http.get('*/v:version/pets/:petId/text', async (info) => {await delay(1000); + + return new HttpResponse(overrideResponse !== undefined + ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) + : getShowPetTextResponseMock(), + { status: 200, + headers: { 'Content-Type': 'text/plain' } + }) + }) +} + +export const getUploadFileMockHandler = (overrideResponse?: null | ((info: Parameters[1]>[0]) => Promise | null)) => { + return http.post('*/v:version/pet/:petId/uploadImage', async (info) => {await delay(1000); + if (typeof overrideResponse === 'function') {await overrideResponse(info); } + return new HttpResponse(null, + { status: 200, + + }) + }) +} + +export const getDownloadFileMockHandler = (overrideResponse?: Blob | ((info: Parameters[1]>[0]) => Promise | Blob)) => { + return http.get('*/v:version/pet/:petId/downloadImage', async (info) => {await delay(1000); + + return new HttpResponse(JSON.stringify(overrideResponse !== undefined + ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) + : getDownloadFileResponseMock()), + { status: 200, + headers: { 'Content-Type': 'application/json' } + }) + }) +} +export const getPetsMock = () => [ + getSearchPetsMockHandler(), + getListPetsMockHandler(), + getCreatePetsMockHandler(), + getShowPetByIdMockHandler(), + getShowPetTextMockHandler(), + getUploadFileMockHandler(), + getDownloadFileMockHandler() +] diff --git a/samples/angular-query/src/api/endpoints/pets/pets.ts b/samples/angular-query/src/api/endpoints/pets/pets.ts new file mode 100644 index 000000000..d305c91dd --- /dev/null +++ b/samples/angular-query/src/api/endpoints/pets/pets.ts @@ -0,0 +1,513 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ +import { + injectMutation, + injectQuery +} from '@tanstack/angular-query-experimental'; +import type { + CreateMutationOptions, + CreateMutationResult, + CreateQueryOptions, + CreateQueryResult, + MutationFunction, + QueryFunction +} from '@tanstack/angular-query-experimental'; + +import type { + CreatePetsBody, + Error, + ListPetsParams, + Pet, + Pets, + SearchPetsParams +} from '../../model'; + +import searchPetsMutator from '../../mutator/custom-instance'; +import listPetsMutator from '../../mutator/custom-instance'; +import createPetsMutator from '../../mutator/custom-instance'; +import showPetByIdMutator from '../../mutator/custom-instance'; +import showPetTextMutator from '../../mutator/custom-instance'; +import uploadFileMutator from '../../mutator/custom-instance'; +import downloadFileMutator from '../../mutator/custom-instance'; +import paramsSerializerMutator from '../../mutator/custom-params-serializer'; + + +type SecondParameter unknown> = Parameters[1]; + + + +/** + * @summary search by query params + */ +export const searchPets = ( + params: SearchPetsParams, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return searchPetsMutator( + {url: `/v${version}/search`, method: 'GET', + params, signal + }, + options); + } + + +export const getSearchPetsQueryKey = (params?: SearchPetsParams, + version: number= 1,) => { + return [`/v${version}/search`, ...(params ? [params]: [])] as const; + } + + +export const getSearchPetsQueryOptions = >, TError = Error>(params: SearchPetsParams, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getSearchPetsQueryKey(params,version); + + + + const queryFn: QueryFunction>> = ({ signal }) => searchPets(params,version, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(version), ...queryOptions} as CreateQueryOptions>, TError, TData> +} + +export type SearchPetsQueryResult = NonNullable>> +export type SearchPetsQueryError = Error + + +/** + * @summary search by query params + */ + +export function injectSearchPets>, TError = Error>( + params: SearchPetsParams, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): CreateQueryResult { + + const queryOptions = getSearchPetsQueryOptions(params,version,options) + + const query = injectQuery(() => queryOptions) as CreateQueryResult; + + + + return query; +} + + + +/** + * @summary List all pets + */ +export const listPets = ( + params?: ListPetsParams, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return listPetsMutator( + {url: `/v${version}/pets`, method: 'GET', + params, signal + }, + options); + } + + +export const getListPetsQueryKey = (params?: ListPetsParams, + version: number= 1,) => { + return [`/v${version}/pets`, ...(params ? [params]: [])] as const; + } + + +export const getListPetsQueryOptions = >, TError = Error>(params?: ListPetsParams, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListPetsQueryKey(params,version); + + + + const queryFn: QueryFunction>> = ({ signal }) => listPets(params,version, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(version), ...queryOptions} as CreateQueryOptions>, TError, TData> +} + +export type ListPetsQueryResult = NonNullable>> +export type ListPetsQueryError = Error + + +/** + * @summary List all pets + */ + +export function injectListPets>, TError = Error>( + params?: ListPetsParams, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): CreateQueryResult { + + const queryOptions = getListPetsQueryOptions(params,version,options) + + const query = injectQuery(() => queryOptions) as CreateQueryResult; + + + + return query; +} + + + +/** + * @summary Create a pet + */ +export const createPets = ( + createPetsBody: CreatePetsBody, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return createPetsMutator( + {url: `/v${version}/pets`, method: 'POST', + headers: {'Content-Type': 'application/json', }, + data: createPetsBody, signal + }, + options); + } + + + +export const getCreatePetsMutationOptions = (options?: { mutation?:CreateMutationOptions>, TError,{data: CreatePetsBody;version?: number}, TContext>, request?: SecondParameter} +): CreateMutationOptions>, TError,{data: CreatePetsBody;version?: number}, TContext> => { + +const mutationKey = ['createPets']; +const {mutation: mutationOptions, request: requestOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, request: undefined}; + + + + + const mutationFn: MutationFunction>, {data: CreatePetsBody;version?: number}> = (props) => { + const {data,version} = props ?? {}; + + return createPets(data,version,requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type CreatePetsMutationResult = NonNullable>> + export type CreatePetsMutationBody = CreatePetsBody + export type CreatePetsMutationError = Error + + /** + * @summary Create a pet + */ +export const injectCreatePets = (options?: { mutation?:CreateMutationOptions>, TError,{data: CreatePetsBody;version?: number}, TContext>, request?: SecondParameter} + ): CreateMutationResult< + Awaited>, + TError, + {data: CreatePetsBody;version?: number}, + TContext + > => { + + const mutationOptions = getCreatePetsMutationOptions(options); + + return injectMutation(() => mutationOptions); + } + /** + * @summary Info for a specific pet + */ +export const showPetById = ( + petId: string, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return showPetByIdMutator( + {url: `/v${version}/pets/${petId}`, method: 'GET', signal + }, + options); + } + + +export const getShowPetByIdQueryKey = (petId?: string, + version: number= 1,) => { + return [`/v${version}/pets/${petId}`] as const; + } + + +export const getShowPetByIdQueryOptions = >, TError = Error>(petId: string, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getShowPetByIdQueryKey(petId,version); + + + + const queryFn: QueryFunction>> = ({ signal }) => showPetById(petId,version, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(version && petId), ...queryOptions} as CreateQueryOptions>, TError, TData> +} + +export type ShowPetByIdQueryResult = NonNullable>> +export type ShowPetByIdQueryError = Error + + +/** + * @summary Info for a specific pet + */ + +export function injectShowPetById>, TError = Error>( + petId: string, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): CreateQueryResult { + + const queryOptions = getShowPetByIdQueryOptions(petId,version,options) + + const query = injectQuery(() => queryOptions) as CreateQueryResult; + + + + return query; +} + + + +/** + * @summary Info for a specific pet + */ +export const showPetText = ( + petId: string, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return showPetTextMutator( + {url: `/v${version}/pets/${petId}/text`, method: 'GET', signal + }, + options); + } + + +export const getShowPetTextQueryKey = (petId?: string, + version: number= 1,) => { + return [`/v${version}/pets/${petId}/text`] as const; + } + + +export const getShowPetTextQueryOptions = >, TError = Error>(petId: string, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getShowPetTextQueryKey(petId,version); + + + + const queryFn: QueryFunction>> = ({ signal }) => showPetText(petId,version, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(version && petId), ...queryOptions} as CreateQueryOptions>, TError, TData> +} + +export type ShowPetTextQueryResult = NonNullable>> +export type ShowPetTextQueryError = Error + + +/** + * @summary Info for a specific pet + */ + +export function injectShowPetText>, TError = Error>( + petId: string, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): CreateQueryResult { + + const queryOptions = getShowPetTextQueryOptions(petId,version,options) + + const query = injectQuery(() => queryOptions) as CreateQueryResult; + + + + return query; +} + + + +/** + * Upload image of the pet. + * @summary Uploads an image. + */ +export const uploadFile = ( + petId: number, + uploadFileBody: Blob, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return uploadFileMutator( + {url: `/v${version}/pet/${petId}/uploadImage`, method: 'POST', + headers: {'Content-Type': 'application/octet-stream', }, + data: uploadFileBody, signal + }, + options); + } + + + +export const getUploadFileMutationOptions = (options?: { mutation?:CreateMutationOptions>, TError,{petId: number;data: Blob;version?: number}, TContext>, request?: SecondParameter} +): CreateMutationOptions>, TError,{petId: number;data: Blob;version?: number}, TContext> => { + +const mutationKey = ['uploadFile']; +const {mutation: mutationOptions, request: requestOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, request: undefined}; + + + + + const mutationFn: MutationFunction>, {petId: number;data: Blob;version?: number}> = (props) => { + const {petId,data,version} = props ?? {}; + + return uploadFile(petId,data,version,requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type UploadFileMutationResult = NonNullable>> + export type UploadFileMutationBody = Blob + export type UploadFileMutationError = null | null | Error + + /** + * @summary Uploads an image. + */ +export const injectUploadFile = (options?: { mutation?:CreateMutationOptions>, TError,{petId: number;data: Blob;version?: number}, TContext>, request?: SecondParameter} + ): CreateMutationResult< + Awaited>, + TError, + {petId: number;data: Blob;version?: number}, + TContext + > => { + + const mutationOptions = getUploadFileMutationOptions(options); + + return injectMutation(() => mutationOptions); + } + /** + * Download image of the pet. + * @summary Download an image. + */ +export const downloadFile = ( + petId: number, + version: number = 1, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return downloadFileMutator( + {url: `/v${version}/pet/${petId}/downloadImage`, method: 'GET', + responseType: 'blob', signal + }, + options); + } + + +export const getDownloadFileQueryKey = (petId?: number, + version: number= 1,) => { + return [`/v${version}/pet/${petId}/downloadImage`] as const; + } + + +export const getDownloadFileQueryOptions = >, TError = null | null | Error>(petId: number, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getDownloadFileQueryKey(petId,version); + + + + const queryFn: QueryFunction>> = ({ signal }) => downloadFile(petId,version, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(version && petId), ...queryOptions} as CreateQueryOptions>, TError, TData> +} + +export type DownloadFileQueryResult = NonNullable>> +export type DownloadFileQueryError = null | null | Error + + +/** + * @summary Download an image. + */ + +export function injectDownloadFile>, TError = null | null | Error>( + petId: number, + version: number = 1, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): CreateQueryResult { + + const queryOptions = getDownloadFileQueryOptions(petId,version,options) + + const query = injectQuery(() => queryOptions) as CreateQueryResult; + + + + return query; +} + + + diff --git a/samples/angular-query/src/api/model/createPetsBody.ts b/samples/angular-query/src/api/model/createPetsBody.ts new file mode 100644 index 000000000..0df8b8bb9 --- /dev/null +++ b/samples/angular-query/src/api/model/createPetsBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ + +export type CreatePetsBody = { + name: string; + tag: string; +}; diff --git a/samples/angular-query/src/api/model/error.ts b/samples/angular-query/src/api/model/error.ts new file mode 100644 index 000000000..1c0d0e33f --- /dev/null +++ b/samples/angular-query/src/api/model/error.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ + +export interface Error { + code: number; + message: string; +} diff --git a/samples/angular-query/src/api/model/index.ts b/samples/angular-query/src/api/model/index.ts new file mode 100644 index 000000000..6be6a1a33 --- /dev/null +++ b/samples/angular-query/src/api/model/index.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ + +export * from './createPetsBody'; +export * from './error'; +export * from './listPetsParams'; +export * from './pet'; +export * from './pets'; +export * from './searchPetsParams'; \ No newline at end of file diff --git a/samples/angular-query/src/api/model/listPetsParams.ts b/samples/angular-query/src/api/model/listPetsParams.ts new file mode 100644 index 000000000..bb225e3bd --- /dev/null +++ b/samples/angular-query/src/api/model/listPetsParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ + +export type ListPetsParams = { +/** + * How many items to return at one time (max 100) + */ +limit?: string; +}; diff --git a/samples/angular-query/src/api/model/pet.ts b/samples/angular-query/src/api/model/pet.ts new file mode 100644 index 000000000..67c27d9fc --- /dev/null +++ b/samples/angular-query/src/api/model/pet.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ + +export interface Pet { + id: number; + name: string; + tag?: string; + /** @nullable */ + requiredNullableString: string | null; + /** @nullable */ + optionalNullableString?: string | null; +} diff --git a/samples/angular-query/src/api/model/pets.ts b/samples/angular-query/src/api/model/pets.ts new file mode 100644 index 000000000..1ae630329 --- /dev/null +++ b/samples/angular-query/src/api/model/pets.ts @@ -0,0 +1,9 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ +import type { Pet } from './pet'; + +export type Pets = Pet[]; diff --git a/samples/angular-query/src/api/model/searchPetsParams.ts b/samples/angular-query/src/api/model/searchPetsParams.ts new file mode 100644 index 000000000..0e0b35125 --- /dev/null +++ b/samples/angular-query/src/api/model/searchPetsParams.ts @@ -0,0 +1,21 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * Swagger Petstore + * OpenAPI spec version: 1.0.0 + */ + +export type SearchPetsParams = { +/** + * @nullable + */ +requirednullableString: string | null; +/** + * @nullable + */ +requirednullableStringTwo: string | null; +/** + * @nullable + */ +nonRequirednullableString?: string | null; +}; diff --git a/samples/angular-query/src/api/mutator/custom-instance.ts b/samples/angular-query/src/api/mutator/custom-instance.ts new file mode 100644 index 000000000..6c4cedd7e --- /dev/null +++ b/samples/angular-query/src/api/mutator/custom-instance.ts @@ -0,0 +1,74 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { lastValueFrom } from 'rxjs'; + +const baseURL = ''; // use your own URL here or environment variable + +export interface AngularRequestConfig { + url: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + params?: any; + data?: any; + headers?: any; + signal?: AbortSignal; + responseType?: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class ApiHttpService { + private http = inject(HttpClient); + + async request(config: AngularRequestConfig, options?: any): Promise { + const { url, method, params, data, headers, signal } = config; + + let targetUrl = `${baseURL}${url}`; + + if (params) { + targetUrl += '?' + new URLSearchParams(params); + } + + // Prepare headers + const httpHeaders = headers ? new HttpHeaders(headers) : undefined; + + const request$ = this.http.request(method, targetUrl, { + headers: httpHeaders, + params, + body: data, + // Add signal for request cancellation support + ...(signal && { signal }), + }); + + return lastValueFrom(request$); + } +} + +// Global service instance - this will be injected properly when used in Angular context +let apiHttpService: ApiHttpService | null = null; + +// Function to set the service instance (called from Angular components/services) +export const setApiHttpService = (service: ApiHttpService) => { + apiHttpService = service; +}; + +// Custom instance function that uses the injected service +export const customInstance = async ( + config: AngularRequestConfig, + options?: any, +): Promise => { + if (!apiHttpService) { + // Try to inject the service if not already set + try { + apiHttpService = inject(ApiHttpService); + } catch (error) { + throw new Error( + 'ApiHttpService not available. Make sure to call setApiHttpService() or use within an Angular injection context.', + ); + } + } + + return apiHttpService.request(config, options); +}; + +export default customInstance; diff --git a/samples/angular-query/src/api/mutator/custom-params-serializer.ts b/samples/angular-query/src/api/mutator/custom-params-serializer.ts new file mode 100644 index 000000000..b5df3e153 --- /dev/null +++ b/samples/angular-query/src/api/mutator/custom-params-serializer.ts @@ -0,0 +1,15 @@ +import type { HttpParams } from '@angular/common/http'; + +type AngularHttpParams = + | HttpParams + | Record< + string, + string | number | boolean | readonly (string | number | boolean)[] + > + | undefined; + +export default function (params: Record): AngularHttpParams { + // do your implementation to transform the params + + return params; +} diff --git a/samples/angular-query/src/api/mutator/response-type.ts b/samples/angular-query/src/api/mutator/response-type.ts new file mode 100644 index 000000000..bee1f4c77 --- /dev/null +++ b/samples/angular-query/src/api/mutator/response-type.ts @@ -0,0 +1,27 @@ +import { HttpClient } from '@angular/common/http'; +import { lastValueFrom, Observable } from 'rxjs'; + +const responseType = ( + { + url, + method, + params, + data, + }: { + url: string; + method: string; + params?: any; + data?: any; + headers?: any; + }, + http: HttpClient, +): Promise => + lastValueFrom( + http.request(method, url, { + params, + body: data, + responseType: 'json', + }), + ); + +export default responseType; diff --git a/samples/angular-query/src/api/node.ts b/samples/angular-query/src/api/node.ts new file mode 100644 index 000000000..0ae82446f --- /dev/null +++ b/samples/angular-query/src/api/node.ts @@ -0,0 +1,6 @@ +import { setupServer } from 'msw/node'; +import { getPetsMock } from './endpoints/pets/pets.msw'; + +const server = setupServer(...getPetsMock()); + +export { server }; diff --git a/samples/angular-query/src/api/transformer/add-version.js b/samples/angular-query/src/api/transformer/add-version.js new file mode 100644 index 000000000..d7c6c1692 --- /dev/null +++ b/samples/angular-query/src/api/transformer/add-version.js @@ -0,0 +1,36 @@ +/** + * Transformer function for orval. + * + * @param {OpenAPIObject} schema + * @return {OpenAPIObject} + */ +module.exports = (inputSchema) => ({ + ...inputSchema, + paths: Object.entries(inputSchema.paths).reduce( + (acc, [path, pathItem]) => ({ + ...acc, + [`v{version}${path}`]: Object.entries(pathItem).reduce( + (pathItemAcc, [verb, operation]) => ({ + ...pathItemAcc, + [verb]: { + ...operation, + parameters: [ + ...(operation.parameters || []), + { + name: 'version', + in: 'path', + required: true, + schema: { + type: 'number', + default: 1, + }, + }, + ], + }, + }), + {}, + ), + }), + {}, + ), +}); diff --git a/samples/angular-query/src/app/app.config.ts b/samples/angular-query/src/app/app.config.ts new file mode 100644 index 000000000..5dbea576a --- /dev/null +++ b/samples/angular-query/src/app/app.config.ts @@ -0,0 +1,34 @@ +import { provideHttpClient } from '@angular/common/http'; +import { + ApplicationConfig, + provideBrowserGlobalErrorListeners, + provideZonelessChangeDetection, + inject, + provideAppInitializer, +} from '@angular/core'; +import { + provideTanStackQuery, + QueryClient, +} from '@tanstack/angular-query-experimental'; +import { + ApiHttpService, + setApiHttpService, +} from '../api/mutator/custom-instance'; + +// Initialize the API service for use in generated code +function initializeApiService() { + return () => { + const apiService = inject(ApiHttpService); + setApiHttpService(apiService); + }; +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideZonelessChangeDetection(), + provideHttpClient(), + provideTanStackQuery(new QueryClient()), + provideAppInitializer(initializeApiService()), + ], +}; diff --git a/samples/angular-query/src/app/app.spec.ts b/samples/angular-query/src/app/app.spec.ts new file mode 100644 index 000000000..a848a7f57 --- /dev/null +++ b/samples/angular-query/src/app/app.spec.ts @@ -0,0 +1,67 @@ +import { + inject, + provideAppInitializer, + provideZonelessChangeDetection, +} from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { App } from './app'; +import { + provideTanStackQuery, + QueryClient, +} from '@tanstack/angular-query-experimental'; +import { + ApiHttpService, + setApiHttpService, +} from '../api/mutator/custom-instance'; + +import { waitFor } from '@testing-library/angular'; + +function initializeApiService() { + return () => { + const apiService = inject(ApiHttpService); + setApiHttpService(apiService); + }; +} + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [ + provideZonelessChangeDetection(), + provideHttpClient(), + provideTanStackQuery(new QueryClient()), + provideAppInitializer(initializeApiService()), + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain( + 'Hello, angular-app', + ); + }); + + it('should render pets', async () => { + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + await fixture.whenStable(); + const compiled = fixture.nativeElement as HTMLElement; + waitFor( + () => { + expect(compiled.querySelectorAll('p').length).toBeGreaterThanOrEqual(1); + }, + { timeout: 3000 }, + ); + }); +}); diff --git a/samples/angular-query/src/app/app.ts b/samples/angular-query/src/app/app.ts new file mode 100644 index 000000000..07ed2327b --- /dev/null +++ b/samples/angular-query/src/app/app.ts @@ -0,0 +1,36 @@ +import { Component, effect, signal } from '@angular/core'; +import { + getListPetsQueryOptions, + injectListPets, +} from '../api/endpoints/pets/pets'; +import { + DataTag, + dataTagSymbol, + injectQuery, +} from '@tanstack/angular-query-experimental'; +import { Pets } from '../api/model'; + +@Component({ + selector: 'app-root', + template: ` +
+

Hello, {{ title() }}

+
+ + @for (pet of pets.data(); track pet.id) { +

{{ pet.name }}

+ } +
+
+ `, +}) +export class App { + protected readonly petsDirectlyInjected = injectListPets(); + protected readonly pets = injectQuery(() => { + const options = getListPetsQueryOptions(); + options.queryKey = ['pets']; + return getListPetsQueryOptions(); + }); + + protected readonly title = signal('angular-app'); +} diff --git a/samples/angular-query/src/index.html b/samples/angular-query/src/index.html new file mode 100644 index 000000000..12c501e68 --- /dev/null +++ b/samples/angular-query/src/index.html @@ -0,0 +1,13 @@ + + + + + AngularApp + + + + + + + + diff --git a/samples/angular-query/src/main.ts b/samples/angular-query/src/main.ts new file mode 100644 index 000000000..cff175cf2 --- /dev/null +++ b/samples/angular-query/src/main.ts @@ -0,0 +1,16 @@ +import { isDevMode } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +async function prepareApp() { + if (isDevMode()) { + const { worker } = await import('./api/browser'); + return worker.start(); + } + + return Promise.resolve(); +} + +await prepareApp(); +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/samples/angular-query/src/styles.css b/samples/angular-query/src/styles.css new file mode 100644 index 000000000..fac28e5a8 --- /dev/null +++ b/samples/angular-query/src/styles.css @@ -0,0 +1,17 @@ +.App { + text-align: center; +} + +.App-header { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: black; +} + +.App-logo { + width: 100%; +} diff --git a/samples/angular-query/tsconfig.app.json b/samples/angular-query/tsconfig.app.json new file mode 100644 index 000000000..a0dcc37c6 --- /dev/null +++ b/samples/angular-query/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] +} diff --git a/samples/angular-query/tsconfig.json b/samples/angular-query/tsconfig.json new file mode 100644 index 000000000..e4955f26b --- /dev/null +++ b/samples/angular-query/tsconfig.json @@ -0,0 +1,34 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/samples/angular-query/tsconfig.spec.json b/samples/angular-query/tsconfig.spec.json new file mode 100644 index 000000000..df2642f0f --- /dev/null +++ b/samples/angular-query/tsconfig.spec.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["vitest/globals"] + }, + "include": ["src/**/*.ts"] +} diff --git a/samples/angular-query/vitest.setup.ts b/samples/angular-query/vitest.setup.ts new file mode 100644 index 000000000..1dafeab57 --- /dev/null +++ b/samples/angular-query/vitest.setup.ts @@ -0,0 +1,6 @@ +import { beforeAll, afterEach, afterAll } from 'vitest'; +import { server } from './src/api/node'; + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/samples/yarn.lock b/samples/yarn.lock index 04b2189e4..a72e9a8db 100644 --- a/samples/yarn.lock +++ b/samples/yarn.lock @@ -206,6 +206,18 @@ __metadata: languageName: node linkType: hard +"@angular/animations@npm:^20.1.4": + version: 20.1.4 + resolution: "@angular/animations@npm:20.1.4" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + "@angular/common": 20.1.4 + "@angular/core": 20.1.4 + checksum: 10c0/da39799e29239f8449c82572bc9fed41762ac94e9c444b48c966bbcff8458b4299e64f1616a6a354fb1f6ce26571d0bd5d709ac837ead6e9d8e7bc214acc8793 + languageName: node + linkType: hard + "@angular/build@npm:^20.1.3": version: 20.1.3 resolution: "@angular/build@npm:20.1.3" @@ -6326,6 +6338,19 @@ __metadata: languageName: node linkType: hard +"@tanstack/angular-query-experimental@npm:^5.62.7": + version: 5.84.1 + resolution: "@tanstack/angular-query-experimental@npm:5.84.1" + dependencies: + "@tanstack/query-core": "npm:5.83.1" + "@tanstack/query-devtools": "npm:5.84.0" + peerDependencies: + "@angular/common": ">=16.0.0" + "@angular/core": ">=16.0.0" + checksum: 10c0/89356ad35ee5e2407fed107b6facc5ab7fa2b64a58595043edd742ad5b3d491f15ca17a8000b031d4ea84de3443e7165583071a4cae3b1caac687730f0cf56ca + languageName: node + linkType: hard + "@tanstack/match-sorter-utils@npm:^8.15.1": version: 8.15.1 resolution: "@tanstack/match-sorter-utils@npm:8.15.1" @@ -6372,6 +6397,20 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.83.1": + version: 5.83.1 + resolution: "@tanstack/query-core@npm:5.83.1" + checksum: 10c0/3eeacf678fcf9803585f1be74979548a33c7f659b8d5f33c1e3b3834ef3b9f3401070b2e2d38bbaa9c6e5a3c359baaa3828078e38007b7c99fb8c9f1e3911142 + languageName: node + linkType: hard + +"@tanstack/query-devtools@npm:5.84.0": + version: 5.84.0 + resolution: "@tanstack/query-devtools@npm:5.84.0" + checksum: 10c0/8257bebbf648e850f0186175a6e17aa6ac50e2dbb9413953927a698a2f73db8009693980f4c6b0f928a703be353a0b1bd1316031029818be7672db98c1b42cde + languageName: node + linkType: hard + "@tanstack/react-query@npm:^5.0.0": version: 5.39.0 resolution: "@tanstack/react-query@npm:5.39.0" @@ -6441,6 +6480,37 @@ __metadata: languageName: node linkType: hard +"@testing-library/angular@npm:16.0.0": + version: 16.0.0 + resolution: "@testing-library/angular@npm:16.0.0" + dependencies: + "@testing-library/dom": "npm:^10.0.0" + tslib: "npm:^2.3.1" + peerDependencies: + "@angular/common": ">= 17.0.0" + "@angular/core": ">= 17.0.0" + "@angular/platform-browser": ">= 17.0.0" + "@angular/router": ">= 17.0.0" + checksum: 10c0/612b3592281ac8ba178d2dcf693bed8255c79fb52a183c0890847ed72e7aad4f27ae76ff95cbc40b152332daf2c747fd172e569e914a748fe0d7eb3ae545e889 + languageName: node + linkType: hard + +"@testing-library/dom@npm:^10.0.0": + version: 10.4.1 + resolution: "@testing-library/dom@npm:10.4.1" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.3.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" + pretty-format: "npm:^27.0.2" + checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1 + languageName: node + linkType: hard + "@testing-library/dom@npm:^9.3.3": version: 9.3.4 resolution: "@testing-library/dom@npm:9.3.4" @@ -8503,6 +8573,32 @@ __metadata: languageName: unknown linkType: soft +"angular-query@workspace:angular-query": + version: 0.0.0-use.local + resolution: "angular-query@workspace:angular-query" + dependencies: + "@angular/animations": "npm:^20.1.4" + "@angular/build": "npm:^20.1.3" + "@angular/cli": "npm:^20.1.3" + "@angular/common": "npm:^20.1.0" + "@angular/compiler": "npm:^20.1.0" + "@angular/compiler-cli": "npm:^20.1.0" + "@angular/core": "npm:^20.1.0" + "@angular/forms": "npm:^20.1.0" + "@angular/platform-browser": "npm:^20.1.0" + "@angular/router": "npm:^20.1.0" + "@faker-js/faker": "npm:^9.9.0" + "@tanstack/angular-query-experimental": "npm:^5.62.7" + "@testing-library/angular": "npm:16.0.0" + msw: "npm:^2.10.4" + orval: "link:../../packages/orval/dist" + rxjs: "npm:~7.8.2" + tslib: "npm:^2.8.1" + typescript: "npm:~5.8.2" + vitest: "npm:^3.2.4" + languageName: unknown + linkType: soft + "ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.1, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" @@ -8625,7 +8721,7 @@ __metadata: languageName: node linkType: hard -"aria-query@npm:^5.3.0": +"aria-query@npm:5.3.0, aria-query@npm:^5.3.0": version: 5.3.0 resolution: "aria-query@npm:5.3.0" dependencies: @@ -17693,6 +17789,12 @@ __metadata: languageName: node linkType: soft +"orval@link:../../packages/orval/dist::locator=angular-query%40workspace%3Aangular-query": + version: 0.0.0-use.local + resolution: "orval@link:../../packages/orval/dist::locator=angular-query%40workspace%3Aangular-query" + languageName: node + linkType: soft + "orval@link:../../packages/orval/dist::locator=basic%40workspace%3Abasic": version: 0.0.0-use.local resolution: "orval@link:../../packages/orval/dist::locator=basic%40workspace%3Abasic" @@ -18110,6 +18212,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:1.1.1, picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + "picocolors@npm:^0.2.1": version: 0.2.1 resolution: "picocolors@npm:0.2.1" @@ -18124,13 +18233,6 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.1.1": - version: 1.1.1 - resolution: "picocolors@npm:1.1.1" - checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 - languageName: node - linkType: hard - "picomatch@npm:4.0.2": version: 4.0.2 resolution: "picomatch@npm:4.0.2" @@ -22578,6 +22680,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.3.1, tslib@npm:^2.8.1": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + "tslib@npm:^2.4.0, tslib@npm:^2.6.3": version: 2.6.3 resolution: "tslib@npm:2.6.3" @@ -22585,13 +22694,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.8.1": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0"