Skip to content

Commit 4fa66db

Browse files
committed
feat: split entity type for endpoint specific types
1 parent 702630e commit 4fa66db

File tree

9 files changed

+106
-22
lines changed

9 files changed

+106
-22
lines changed

src/generator/03-entities/index.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
isReferenceObject,
1111
isRelatedEntitySchema
1212
} from '@utils/openapi/guards';
13-
import { camelCase } from 'change-case';
13+
import { camelCase, pascalCase } from 'change-case';
1414
import { OpenAPIV3 } from 'openapi-types';
1515
import { loosePascalCase } from '@utils/case';
1616

@@ -19,6 +19,7 @@ export interface GeneratedEntity {
1919
properties: Map<string, PropertyMetaData>;
2020
parentName?: string;
2121
source: string;
22+
createProperties: InterfaceProperty[];
2223
}
2324

2425
export const generateEntities = (
@@ -78,12 +79,67 @@ export const generateEntities = (
7879
entities.set(schemaName, {
7980
name: schemaName,
8081
properties,
82+
createProperties: entityInterfaceProperties.filter((prop) => !prop.readonly),
8183
parentName: parentEntityInterfaceName ? camelCase(parentEntityInterfaceName) : undefined,
8284
source: generateStatements(
83-
generateInterface(entityInterfaceName, entityInterfaceProperties, parentEntityInterfaceName)
85+
generateInterface(
86+
entityInterfaceName,
87+
entityInterfaceProperties.map((prop) => ({
88+
...prop,
89+
required: !!(prop.required ?? prop.type?.endsWith('[]')) || prop.type !== 'string'
90+
})),
91+
parentEntityInterfaceName
92+
)
8493
)
8594
});
8695
}
8796

8897
return entities;
8998
};
99+
100+
export const generateCreateEntityStatements = (
101+
entities: Map<string, GeneratedEntity>,
102+
enums: Map<string, GeneratedEnum>
103+
): string[] => {
104+
const isPrimitiveOrEnumOrObject = (propType?: string) => {
105+
return (
106+
!propType ||
107+
enums.has(propType) ||
108+
propType === 'string' ||
109+
propType === 'number' ||
110+
propType === 'boolean' ||
111+
propType === '{}'
112+
);
113+
};
114+
115+
const transformCreateEntityInterfaceProps = (props: InterfaceProperty[]) => {
116+
return props.map((prop) => {
117+
if (isPrimitiveOrEnumOrObject(prop.type)) {
118+
return prop;
119+
}
120+
121+
if (prop.type?.endsWith('[]')) {
122+
const typeWithoutBrackets = prop.type.replace(/^\((.*?)\)\[\]$/, '$1');
123+
return isPrimitiveOrEnumOrObject(typeWithoutBrackets) ? prop : { ...prop, type: `(${typeWithoutBrackets}_Create)[]` };
124+
}
125+
126+
return { ...prop, type: `${prop.type}_Create` };
127+
});
128+
};
129+
130+
const createEntityStatements: string[] = [];
131+
132+
const generateCreateEntityStatement = (entity: GeneratedEntity, entityName: string): string => {
133+
const name = `${pascalCase(entityName)}_Create`;
134+
const parentName = entity.parentName ? `${pascalCase(entity.parentName)}_Create` : undefined;
135+
const interfaceProperties = Array.from(transformCreateEntityInterfaceProps(entity.createProperties));
136+
137+
return generateStatements(generateInterface(name, interfaceProperties, parentName));
138+
};
139+
140+
entities.forEach((entity, name) => {
141+
createEntityStatements.push(generateCreateEntityStatement(entity, name));
142+
});
143+
144+
return createEntityStatements;
145+
};

src/generator/04-services/endpoints/count.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export const generateCountEndpoint: ServiceFunctionGenerator = ({
3030
const functionTypeSource = generateArrowFunctionType({
3131
type: functionTypeName,
3232
params: [
33-
`query${parametersType.isFullyOptional() ? '?' : ''}: CountQuery<${filterTypeName}>${path.parameters?.length ? ' & ' + parametersTypeName : ''}`, 'requestOptions?: RequestOptions'
33+
`query${parametersType.isFullyOptional() ? '?' : ''}: CountQuery<${filterTypeName}>${path.parameters?.length ? ' & ' + parametersTypeName : ''}`,
34+
'requestOptions?: RequestOptions'
3435
],
3536
returns: `${resolveResponseType(target)}<number>`
3637
});

src/generator/04-services/endpoints/create.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import { resolveResponseType } from '../../../target';
22
import { GeneratedServiceFunction, ServiceFunctionGenerator } from '@generator/04-services/types';
3-
import { generateRequestBodyType } from '@generator/04-services/utils/generateRequestBodyType';
43
import { generateResponseBodyType } from '@generator/04-services/utils/generateResponseBodyType';
54
import { generateArrowFunction } from '@ts/generateArrowFunction';
65
import { generateArrowFunctionType } from '@ts/generateArrowFunctionType';
76
import { generateString } from '@ts/generateString';
87
import { pascalCase } from 'change-case';
8+
import { generateRequestBodyType } from '@generator/04-services/utils/generateRequestBodyType';
99

1010
export const generateCreateEndpoint: ServiceFunctionGenerator = ({
1111
target,
1212
path,
1313
endpoint
1414
}): GeneratedServiceFunction => {
15+
const endpointName = pascalCase(endpoint.service);
1516
const functionName = 'create';
16-
const functionTypeName = `${pascalCase(endpoint.service)}Service_${pascalCase(functionName)}`;
17+
const functionTypeName = `${endpointName}Service_${pascalCase(functionName)}`;
1718

1819
const functionTypeSource = generateArrowFunctionType({
1920
type: functionTypeName,
20-
params: [`data: DeepPartial<${generateRequestBodyType(path).toString()}>`, 'requestOptions?: RequestOptions'],
21+
params: [`data: ${generateRequestBodyType(path).toString()}_Create`, 'requestOptions?: RequestOptions'],
2122
returns: `${resolveResponseType(target)}<${generateResponseBodyType(path).toString()}>`
2223
});
2324

@@ -29,7 +30,7 @@ export const generateCreateEndpoint: ServiceFunctionGenerator = ({
2930
});
3031

3132
return {
32-
entity: pascalCase(endpoint.service),
33+
entity: endpointName,
3334
name: functionName,
3435
type: { name: functionTypeName, source: functionTypeSource },
3536
func: { name: functionName, source: functionSource }

src/generator/04-services/endpoints/generic.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@ export const generateGenericEndpoint =
3131
const hasId = endpoint.path.includes('{id}');
3232

3333
const params = createObjectType({
34-
params: convertToTypeScriptType(convertParametersToSchema(path.parameters)),
35-
body: method === 'get' ? undefined : wrapBody(generateRequestBodyType(path), target)
34+
params: convertToTypeScriptType(convertParametersToSchema(path.parameters), undefined, true),
35+
body: method === 'get' ? undefined : wrapBody(generateRequestBodyType(path, true), target)
3636
});
3737

3838
const responseBody = generateResponseBodyType(path);
3939

4040
const functionTypeSource = generateArrowFunctionType({
4141
type: functionTypeName,
42-
params: [...(hasId ? ['id: string'] : []), `query${params.isFullyOptional() ? '?' : ''}: ${entityQuery}`, 'requestOptions?: RequestOptions'],
42+
params: [
43+
...(hasId ? ['id: string'] : []),
44+
`query${params.isFullyOptional() ? '?' : ''}: ${entityQuery}`,
45+
'requestOptions?: RequestOptions'
46+
],
4347
returns: `${resolveResponseType(target)}<${wrapBody(responseBody, target).toString()}>`
4448
});
4549

src/generator/04-services/endpoints/update.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { resolveResponseType } from '../../../target';
22
import { GeneratedServiceFunction, ServiceFunctionGenerator } from '@generator/04-services/types';
3-
import { generateRequestBodyType } from '@generator/04-services/utils/generateRequestBodyType';
43
import { generateResponseBodyType } from '@generator/04-services/utils/generateResponseBodyType';
54
import { insertPathPlaceholder } from '@generator/04-services/utils/insertPathPlaceholder';
65
import { generateArrowFunction } from '@ts/generateArrowFunction';
76
import { generateArrowFunctionType } from '@ts/generateArrowFunctionType';
87
import { pascalCase } from 'change-case';
8+
import { generateRequestBodyType } from '@generator/04-services/utils/generateRequestBodyType';
99

1010
export const generateUpdateEndpoint: ServiceFunctionGenerator = ({
1111
target,
@@ -17,7 +17,12 @@ export const generateUpdateEndpoint: ServiceFunctionGenerator = ({
1717

1818
const functionTypeSource = generateArrowFunctionType({
1919
type: functionTypeName,
20-
params: ['id: string', `data: DeepPartial<${generateRequestBodyType(path).toString()}>`, 'options?: UpdateQuery', 'requestOptions?: RequestOptions'],
20+
params: [
21+
'id: string',
22+
`data: ${generateRequestBodyType(path).toString()}`,
23+
'options?: UpdateQuery',
24+
'requestOptions?: RequestOptions'
25+
],
2126
returns: `${resolveResponseType(target)}<${generateResponseBodyType(path).toString()}>`
2227
});
2328

src/generator/04-services/utils/generateBodyType.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { isReferenceObject } from '@utils/openapi/guards';
33
import { OpenAPIV3 } from 'openapi-types';
44

55
export const generateBodyType = (
6-
body?: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject | OpenAPIV3.ResponseObject
6+
body?: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject | OpenAPIV3.ResponseObject,
7+
isDeepPartialType?: boolean
78
): AnyType | undefined => {
89
if (isReferenceObject(body)) {
910
return convertToTypeScriptType(body);
@@ -12,7 +13,7 @@ export const generateBodyType = (
1213
const types: AnyType[] = [];
1314
for (const { schema } of Object.values(body?.content ?? {})) {
1415
if (schema) {
15-
types.push(convertToTypeScriptType(schema));
16+
types.push(convertToTypeScriptType(schema, undefined, isDeepPartialType));
1617
}
1718
}
1819

src/generator/04-services/utils/generateRequestBodyType.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { generateBodyType } from '@generator/04-services/utils/generateBodyType'
22
import { AnyType, createRawType } from '@utils/openapi/convertToTypeScriptType';
33
import { OpenAPIV3 } from 'openapi-types';
44

5-
export const generateRequestBodyType = ({ requestBody }: OpenAPIV3.OperationObject): AnyType => {
6-
return generateBodyType(requestBody) ?? createRawType('unknown');
5+
export const generateRequestBodyType = (
6+
{ requestBody }: OpenAPIV3.OperationObject,
7+
isDeepPartialType?: boolean
8+
): AnyType => {
9+
return generateBodyType(requestBody, isDeepPartialType) ?? createRawType('unknown');
710
};

src/generator/generate.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Target } from '../target';
22
import { generateBase } from '@generator/01-base';
33
import { generateEnums } from '@generator/02-enums';
4-
import { generateEntities } from '@generator/03-entities';
4+
import { generateCreateEntityStatements, generateEntities } from '@generator/03-entities';
55
import { generateServices } from '@generator/04-services';
66
import { generateMaps } from '@generator/05-maps';
77
import { generateBlockComment } from '@ts/generateComment';
@@ -25,13 +25,17 @@ export const generate = (doc: OpenAPIV3.Document, options: GeneratorOptions): st
2525

2626
const enums = generateEnums(schemas);
2727
const entities = generateEntities(schemas, enums);
28+
const createEntityStatements = generateCreateEntityStatements(entities, enums);
2829
const services = generateServices(doc.paths, entities, aliases, options);
2930
const maps = generateMaps(enums, entities, services, aliases, options);
3031

3132
return generateStatements(
3233
generateBase(options.target, doc.info.version, options),
3334
generateBlockComment('ENUMS', generateStatements(...[...enums.values()].map((v) => v.source))),
34-
generateBlockComment('ENTITIES', generateStatements(...[...entities.values()].map((v) => v.source))),
35+
generateBlockComment(
36+
'ENTITIES',
37+
generateStatements(...[...entities.values()].map((v) => v.source), ...createEntityStatements)
38+
),
3539
generateBlockComment('SERVICES', generateStatements(...[...services.values()].map((v) => v.source))),
3640
generateBlockComment('MAPS', generateStatements(maps.source))
3741
);

src/utils/openapi/convertToTypeScriptType.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export const createTupleType = (value: (Type | string)[]): TupleType => ({
5353
toString: () => concat([...new Set(value.map((v) => (typeof v === 'string' ? `'${v}'` : v.toString())))], ' | ')
5454
});
5555

56-
export const createObjectType = (value: Record<string, Type | undefined>, required: string[] = []): ObjectType => ({
56+
export const createObjectType = (
57+
value: Record<string, Type | undefined>,
58+
required: string[] = [],
59+
isDeepPartialType?: boolean
60+
): ObjectType => ({
5761
type: 'object',
5862
isFullyOptional: () => {
5963
return (
@@ -72,7 +76,10 @@ export const createObjectType = (value: Record<string, Type | undefined>, requir
7276
const isRequired =
7377
required.includes(name) ||
7478
(value.type === 'object' && !value.isFullyOptional() && propagateOptionalProperties);
75-
return `${name + (isRequired ? '' : '?')}: ${value.toString()};`;
79+
const valueString =
80+
isDeepPartialType && value.type === 'reference' ? `DeepPartial<${value.toString()}>` : value.toString();
81+
82+
return `${name + (isRequired ? '' : '?')}: ${valueString};`;
7683
});
7784

7885
return properties.length ? `{\n${indent(properties.join('\n'))}\n}` : '{}';
@@ -87,7 +94,8 @@ export const getRefName = (obj: OpenAPIV3.ReferenceObject) => {
8794

8895
export const convertToTypeScriptType = (
8996
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
90-
property?: string
97+
property?: string,
98+
isDeepPartialType?: boolean
9199
): AnyType => {
92100
if (isReferenceObject(schema)) {
93101
return createReferenceType(getRefName(schema));
@@ -108,7 +116,8 @@ export const convertToTypeScriptType = (
108116
const { properties = {}, required = [] } = schema;
109117
return createObjectType(
110118
Object.fromEntries(Object.entries(properties).map((v) => [v[0], convertToTypeScriptType(v[1])])),
111-
required
119+
required,
120+
isDeepPartialType
112121
);
113122
}
114123
case 'array':

0 commit comments

Comments
 (0)