Skip to content

Commit 1a01aa5

Browse files
committed
feat: add sdk.params option
1 parent 729488e commit 1a01aa5

File tree

9 files changed

+185
-63
lines changed

9 files changed

+185
-63
lines changed

.changeset/olive-buckets-doubt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hey-api/openapi-ts': patch
3+
---
4+
5+
fix: make data types of never "required" so they don't accept undefined

packages/openapi-ts-tests/test/openapi-ts.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ export default defineConfig(() => {
8585
// auth: false,
8686
// client: false,
8787
// include...
88-
// name: '@hey-api/sdk',
88+
name: '@hey-api/sdk',
8989
// operationId: false,
90+
params: 'flattened',
9091
// serviceNameBuilder: '^Parameters',
9192
// throwOnError: true,
9293
// transformer: '@hey-api/transformers',

packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const defaultConfig: Plugin.Config<Config> = {
4646
name: '@hey-api/sdk',
4747
operationId: true,
4848
output: 'sdk',
49+
params: 'namespaced',
4950
response: 'body',
5051
serviceNameBuilder: '{{name}}Service',
5152
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { FunctionParameter } from '../../../compiler';
2+
import { clientApi } from '../../../generate/client';
3+
import type { TypeScriptFile } from '../../../generate/files';
4+
import { hasOperationDataRequired } from '../../../ir/operation';
5+
import type { IR } from '../../../ir/types';
6+
import type { Plugin } from '../../types';
7+
import { getClientPlugin } from '../client-core/utils';
8+
import {
9+
importIdentifierData,
10+
importIdentifierResponse,
11+
} from '../typescript/ref';
12+
import { nuxtTypeComposable, nuxtTypeDefault } from './constants';
13+
import type { Config } from './types';
14+
15+
export const operationOptionsType = ({
16+
context,
17+
file,
18+
operation,
19+
throwOnError,
20+
transformData,
21+
}: {
22+
context: IR.Context;
23+
file: TypeScriptFile;
24+
operation: IR.OperationObject;
25+
throwOnError?: string;
26+
transformData?: (name: string) => string;
27+
}) => {
28+
const identifierData = importIdentifierData({ context, file, operation });
29+
const identifierResponse = importIdentifierResponse({
30+
context,
31+
file,
32+
operation,
33+
});
34+
35+
const optionsName = clientApi.Options.name;
36+
37+
const finalData = (name: string) =>
38+
transformData ? transformData(name) : name;
39+
40+
const client = getClientPlugin(context.config);
41+
if (client.name === '@hey-api/client-nuxt') {
42+
return `${optionsName}<${nuxtTypeComposable}, ${identifierData.name ? finalData(identifierData.name) : 'unknown'}, ${identifierResponse.name || 'unknown'}, ${nuxtTypeDefault}>`;
43+
}
44+
45+
// TODO: refactor this to be more generic, works for now
46+
if (throwOnError) {
47+
return `${optionsName}<${identifierData.name ? finalData(identifierData.name) : 'unknown'}, ${throwOnError}>`;
48+
}
49+
return identifierData.name
50+
? `${optionsName}<${finalData(identifierData.name)}>`
51+
: optionsName;
52+
};
53+
54+
export const createParameters = ({
55+
context,
56+
file,
57+
operation,
58+
plugin,
59+
}: {
60+
context: IR.Context;
61+
file: TypeScriptFile;
62+
operation: IR.OperationObject;
63+
plugin: Plugin.Instance<Config>;
64+
}): Array<FunctionParameter> | undefined => {
65+
const client = getClientPlugin(context.config);
66+
const isNuxtClient = client.name === '@hey-api/client-nuxt';
67+
68+
const parameters: Array<FunctionParameter> = [];
69+
70+
if (plugin.params === 'flattened') {
71+
const identifierData = importIdentifierData({ context, file, operation });
72+
73+
if (identifierData.name) {
74+
parameters.push({
75+
isRequired: hasOperationDataRequired(operation),
76+
name: 'params',
77+
type: `OmitNever<Omit<${identifierData.name}, 'url'>>`,
78+
});
79+
}
80+
81+
parameters.push({
82+
isRequired: !plugin.client || isNuxtClient,
83+
name: 'options',
84+
type: operationOptionsType({
85+
context,
86+
file,
87+
operation,
88+
throwOnError: isNuxtClient ? undefined : 'ThrowOnError',
89+
transformData: (name) => `Pick<${name}, 'url'>`,
90+
}),
91+
});
92+
}
93+
94+
if (plugin.params === 'namespaced') {
95+
const isRequiredOptions =
96+
!plugin.client || isNuxtClient || hasOperationDataRequired(operation);
97+
parameters.push({
98+
isRequired: isRequiredOptions,
99+
name: 'options',
100+
type: operationOptionsType({
101+
context,
102+
file,
103+
operation,
104+
throwOnError: isNuxtClient ? undefined : 'ThrowOnError',
105+
}),
106+
});
107+
}
108+
109+
return parameters.length ? parameters : undefined;
110+
};

packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type ts from 'typescript';
33
import { compiler } from '../../../compiler';
44
import type { ObjectValue } from '../../../compiler/types';
55
import { clientApi, clientModulePath } from '../../../generate/client';
6-
import type { TypeScriptFile } from '../../../generate/files';
76
import {
87
hasOperationDataRequired,
98
statusCodeToGroup,
@@ -21,13 +20,13 @@ import {
2120
transformersId,
2221
} from '../transformers/plugin';
2322
import {
24-
importIdentifierData,
2523
importIdentifierError,
2624
importIdentifierResponse,
2725
} from '../typescript/ref';
2826
import { nuxtTypeComposable, nuxtTypeDefault } from './constants';
27+
import { createParameters } from './params';
2928
import { serviceFunctionIdentifier } from './plugin-legacy';
30-
import { createTypeOptions } from './typeOptions';
29+
import { createTypeOmitNever, createTypeOptions } from './typeOptions';
3130
import type { Config } from './types';
3231

3332
// copy-pasted from @hey-api/client-core
@@ -48,40 +47,6 @@ export interface Auth {
4847
type: 'apiKey' | 'http';
4948
}
5049

51-
export const operationOptionsType = ({
52-
context,
53-
file,
54-
operation,
55-
throwOnError,
56-
}: {
57-
context: IR.Context;
58-
file: TypeScriptFile;
59-
operation: IR.OperationObject;
60-
throwOnError?: string;
61-
}) => {
62-
const identifierData = importIdentifierData({ context, file, operation });
63-
const identifierResponse = importIdentifierResponse({
64-
context,
65-
file,
66-
operation,
67-
});
68-
69-
const optionsName = clientApi.Options.name;
70-
71-
const client = getClientPlugin(context.config);
72-
if (client.name === '@hey-api/client-nuxt') {
73-
return `${optionsName}<${nuxtTypeComposable}, ${identifierData.name || 'unknown'}, ${identifierResponse.name || 'unknown'}, ${nuxtTypeDefault}>`;
74-
}
75-
76-
// TODO: refactor this to be more generic, works for now
77-
if (throwOnError) {
78-
return `${optionsName}<${identifierData.name || 'unknown'}, ${throwOnError}>`;
79-
}
80-
return identifierData.name
81-
? `${optionsName}<${identifierData.name}>`
82-
: optionsName;
83-
};
84-
8550
export const sdkId = 'sdk';
8651

8752
/**
@@ -552,18 +517,12 @@ const generateClassSdk = ({
552517
id: operation.id,
553518
operation,
554519
}),
555-
parameters: [
556-
{
557-
isRequired: isRequiredOptions,
558-
name: 'options',
559-
type: operationOptionsType({
560-
context,
561-
file,
562-
operation,
563-
throwOnError: isNuxtClient ? undefined : 'ThrowOnError',
564-
}),
565-
},
566-
],
520+
parameters: createParameters({
521+
context,
522+
file,
523+
operation,
524+
plugin,
525+
}),
567526
returnType: undefined,
568527
statements: operationStatements({
569528
context,
@@ -658,18 +617,12 @@ const generateFlatSdk = ({
658617
],
659618
exportConst: true,
660619
expression: compiler.arrowFunction({
661-
parameters: [
662-
{
663-
isRequired: isRequiredOptions,
664-
name: 'options',
665-
type: operationOptionsType({
666-
context,
667-
file,
668-
operation,
669-
throwOnError: isNuxtClient ? undefined : 'ThrowOnError',
670-
}),
671-
},
672-
],
620+
parameters: createParameters({
621+
context,
622+
file,
623+
operation,
624+
plugin,
625+
}),
673626
returnType: undefined,
674627
statements: operationStatements({
675628
context,
@@ -752,6 +705,9 @@ export const handler: Plugin.Handler<Config> = ({ context, plugin }) => {
752705
context,
753706
plugin,
754707
});
708+
createTypeOmitNever({
709+
context,
710+
});
755711

756712
if (plugin.asClass) {
757713
generateClassSdk({ context, plugin });

packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import ts from 'typescript';
2+
13
import { compiler } from '../../../compiler';
24
import { clientModulePath } from '../../../generate/client';
35
import type { FileImportResult } from '../../../generate/files';
@@ -131,3 +133,41 @@ export const createTypeOptions = ({
131133

132134
file.add(typeOptions);
133135
};
136+
137+
export const createTypeOmitNever = ({ context }: { context: IR.Context }) => {
138+
const file = context.file({ id: sdkId })!;
139+
140+
const neverType = compiler.keywordTypeNode({ keyword: 'never' });
141+
const kType = compiler.typeReferenceNode({ typeName: 'K' });
142+
const tType = compiler.typeReferenceNode({ typeName: 'T' });
143+
const kOfTType = compiler.indexedAccessTypeNode({
144+
indexType: kType,
145+
objectType: tType,
146+
});
147+
148+
const omitNeverTypeAlias = compiler.typeAliasDeclaration({
149+
exportType: true,
150+
name: 'OmitNever',
151+
type: ts.factory.createMappedTypeNode(
152+
undefined,
153+
ts.factory.createTypeParameterDeclaration(
154+
undefined,
155+
'K',
156+
ts.factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, tType),
157+
undefined,
158+
),
159+
ts.factory.createConditionalTypeNode(
160+
kOfTType,
161+
neverType,
162+
neverType,
163+
kType,
164+
),
165+
undefined,
166+
kOfTType,
167+
undefined,
168+
),
169+
typeParameters: [{ name: 'T' }],
170+
});
171+
172+
file.add(omitNeverTypeAlias);
173+
};

packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export interface Config extends Plugin.Name<'@hey-api/sdk'> {
6969
* @default 'sdk'
7070
*/
7171
output?: string;
72+
/**
73+
* TODO
74+
*
75+
* @default 'namespaced'
76+
*/
77+
params?: 'flattened' | 'namespaced';
7278
/**
7379
* Customize the generated service class names. The name variable is
7480
* obtained from your OpenAPI specification tags.

packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,7 @@ const operationToDataType = ({
852852
data.properties.body = {
853853
type: 'never',
854854
};
855+
dataRequired.push('body');
855856
}
856857

857858
// TODO: parser - handle cookie parameters
@@ -879,6 +880,7 @@ const operationToDataType = ({
879880
data.properties.path = {
880881
type: 'never',
881882
};
883+
dataRequired.push('path');
882884
}
883885

884886
if (operation.parameters?.query) {
@@ -893,6 +895,7 @@ const operationToDataType = ({
893895
data.properties.query = {
894896
type: 'never',
895897
};
898+
dataRequired.push('query');
896899
}
897900

898901
data.properties.url = {

packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ImportExportItemObject } from '../../../compiler/utils';
22
import type { IR } from '../../../ir/types';
33
import { getClientPlugin } from '../../@hey-api/client-core/utils';
4-
import { operationOptionsType } from '../../@hey-api/sdk/plugin';
4+
import { operationOptionsType } from '../../@hey-api/sdk/params';
55
import {
66
importIdentifierError,
77
importIdentifierResponse,

0 commit comments

Comments
 (0)