Skip to content

Commit ec6ba46

Browse files
fix(angular): keep httpResource type imports type-only
1 parent 101cade commit ec6ba46

4 files changed

Lines changed: 121 additions & 45 deletions

File tree

packages/angular/src/http-resource.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,40 @@ describe('angular httpResource generator', () => {
429429
expect(importNames).toContain('Pet');
430430
expect(importNames).not.toContain('Error');
431431
});
432+
433+
it('keeps zod array response imports type-only when parse is not generated', async () => {
434+
const output = createOutput({
435+
schemas: {
436+
type: 'zod',
437+
path: '/tmp/schemas',
438+
} as NormalizedOutputOptions['schemas'],
439+
});
440+
const verbOption = createVerbOption({
441+
response: baseResponse({
442+
imports: [{ name: 'Pet' }],
443+
definition: { success: 'Pet[]', errors: 'Error' },
444+
types: {
445+
success: [createSuccessType('Pet[]', 'application/json')],
446+
errors: [],
447+
},
448+
}),
449+
});
450+
451+
const result = await generateHttpResourceClient(
452+
verbOption,
453+
createGeneratorOptions({
454+
route: '/api/pets',
455+
context: createContextSpec(output),
456+
override: output.override,
457+
output: output.target,
458+
}),
459+
'angular',
460+
output,
461+
);
462+
463+
expect(result.imports).toContainEqual({ name: 'Pet' });
464+
expect(result.imports).not.toContainEqual({ name: 'Pet', values: true });
465+
});
432466
});
433467

434468
// ─── Route registry fallback ──────────────────────────────────────

packages/angular/src/http-resource.ts

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -542,25 +542,86 @@ const getHttpResourceResponseImports = (
542542
});
543543
};
544544

545+
const getParseSchemaName = (
546+
response: {
547+
readonly imports: readonly { name: string; isZodSchema?: boolean }[];
548+
readonly definition: { readonly success?: string };
549+
},
550+
factory: HttpResourceFactoryName,
551+
output: NormalizedOutputOptions,
552+
responseTypeOverride?: string,
553+
): string | undefined => {
554+
if (factory !== 'httpResource') return undefined;
555+
556+
// Explicit isZodSchema flag on imports (forward-compatible)
557+
const zodSchema = response.imports.find((imp) => imp.isZodSchema);
558+
if (zodSchema) return zodSchema.name;
559+
560+
// Check if runtime validation is disabled
561+
if (!output.override.angular.runtimeValidation) return undefined;
562+
563+
// Auto-detect: when schemas.type === 'zod', use the response type as the schema name
564+
if (!isZodSchemaOutput(output)) return undefined;
565+
566+
const responseType = responseTypeOverride ?? response.definition.success;
567+
if (!responseType) return undefined;
568+
if (isPrimitiveType(responseType)) return undefined;
569+
570+
// Verify a matching import exists (the response type name resolves to a zod schema)
571+
const hasMatchingImport = response.imports.some(
572+
(imp) => imp.name === responseType,
573+
);
574+
if (!hasMatchingImport) return undefined;
575+
576+
return responseType;
577+
};
578+
579+
const getHttpResourceZodParsedImportNames = (
580+
response: GeneratorVerbOptions['response'],
581+
output: NormalizedOutputOptions,
582+
): Set<string> => {
583+
const names = new Set<string>();
584+
585+
for (const successType of response.types.success) {
586+
const schemaName = getParseSchemaName(
587+
response,
588+
getHttpResourceFactory(
589+
response,
590+
successType.contentType,
591+
successType.value,
592+
),
593+
output,
594+
successType.value,
595+
);
596+
597+
if (schemaName) {
598+
names.add(schemaName);
599+
}
600+
}
601+
602+
return names;
603+
};
604+
545605
const getHttpResourceVerbImports = (
546606
verbOptions: GeneratorVerbOptions,
547607
output: NormalizedOutputOptions,
548608
): GeneratorImport[] => {
549609
const { response, body, queryParams, props, headers, params } = verbOptions;
550-
const responseImports = isZodSchemaOutput(output)
551-
? [
552-
...getHttpResourceResponseImports(response).map((imp) => ({
553-
...imp,
554-
values: true,
555-
})),
556-
...getHttpResourceResponseImports(response)
557-
.filter((imp) => !isPrimitiveType(imp.name))
558-
.map((imp) => ({ name: getSchemaOutputTypeRef(imp.name) })),
559-
]
560-
: getHttpResourceResponseImports(response);
610+
const responseImports = getHttpResourceResponseImports(response);
611+
const parsedZodImportNames = isZodSchemaOutput(output)
612+
? getHttpResourceZodParsedImportNames(response, output)
613+
: new Set<string>();
614+
const parsedZodImports = responseImports.filter((imp) =>
615+
parsedZodImportNames.has(imp.name),
616+
);
561617

562618
return [
563-
...responseImports,
619+
...responseImports.map((imp) =>
620+
parsedZodImportNames.has(imp.name) ? { ...imp, values: true } : imp,
621+
),
622+
...parsedZodImports
623+
.filter((imp) => !isPrimitiveType(imp.name))
624+
.map((imp) => ({ name: getSchemaOutputTypeRef(imp.name) })),
564625
...body.imports,
565626
...props.flatMap((prop) =>
566627
prop.type === GetterPropType.NAMED_PATH_PARAMS
@@ -583,29 +644,14 @@ const getParseExpression = (
583644
output: NormalizedOutputOptions,
584645
responseTypeOverride?: string,
585646
): string | undefined => {
586-
if (factory !== 'httpResource') return undefined;
587-
588-
// Explicit isZodSchema flag on imports (forward-compatible)
589-
const zodSchema = response.imports.find((imp) => imp.isZodSchema);
590-
if (zodSchema) return `${zodSchema.name}.parse`;
591-
592-
// Check if runtime validation is disabled
593-
if (!output.override.angular.runtimeValidation) return undefined;
594-
595-
// Auto-detect: when schemas.type === 'zod', use the response type as the schema name
596-
if (!isZodSchemaOutput(output)) return undefined;
597-
598-
const responseType = responseTypeOverride ?? response.definition.success;
599-
if (!responseType) return undefined;
600-
if (isPrimitiveType(responseType)) return undefined;
601-
602-
// Verify a matching import exists (the response type name resolves to a zod schema)
603-
const hasMatchingImport = response.imports.some(
604-
(imp) => imp.name === responseType,
647+
const schemaName = getParseSchemaName(
648+
response,
649+
factory,
650+
output,
651+
responseTypeOverride,
605652
);
606-
if (!hasMatchingImport) return undefined;
607653

608-
return `${responseType}.parse`;
654+
return schemaName ? `${schemaName}.parse` : undefined;
609655
};
610656

611657
/**

tests/__snapshots__/angular/http-resource-zod-disabled/endpoints.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ import {
3131
Observable
3232
} from 'rxjs';
3333

34-
import {
35-
Pet,
36-
PetWithTag,
37-
Pets
38-
} from './model';
3934
import type {
4035
CreatePetsBody,
4136
CreatePetsParams,
42-
ListPetsParams
37+
ListPetsParams,
38+
Pet,
39+
PetWithTag,
40+
Pets
4341
} from './model';
4442

4543
import {

tests/__snapshots__/angular/http-resource-zod/endpoints.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ import {
3131
Observable
3232
} from 'rxjs';
3333

34-
import {
35-
Pet,
36-
PetWithTag,
37-
Pets
38-
} from './model';
3934
import type {
4035
CreatePetsBody,
4136
CreatePetsParams,
42-
ListPetsParams
37+
ListPetsParams,
38+
Pet,
39+
PetWithTag,
40+
Pets
4341
} from './model';
4442

4543
import {

0 commit comments

Comments
 (0)