@qualisero/openapi-endpoint 0.13.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -236
- package/dist/cli.js +747 -202
- package/dist/index.d.ts +9 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -58
- package/dist/openapi-helpers.d.ts +4 -17
- package/dist/openapi-helpers.d.ts.map +1 -1
- package/dist/openapi-helpers.js +3 -125
- package/dist/openapi-mutation.d.ts +28 -59
- package/dist/openapi-mutation.d.ts.map +1 -1
- package/dist/openapi-mutation.js +67 -83
- package/dist/openapi-query.d.ts +22 -50
- package/dist/openapi-query.d.ts.map +1 -1
- package/dist/openapi-query.js +22 -55
- package/dist/openapi-utils.d.ts +2 -1
- package/dist/openapi-utils.d.ts.map +1 -1
- package/dist/types.d.ts +223 -467
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -28
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -203,7 +203,7 @@ function addMissingOperationIds(openApiSpec, prefixToStrip = '/api') {
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
|
-
function
|
|
206
|
+
function _parseOperationsFromSpec(openapiContent, excludePrefix = '_deprecated') {
|
|
207
207
|
const openApiSpec = JSON.parse(openapiContent);
|
|
208
208
|
if (!openApiSpec.paths) {
|
|
209
209
|
throw new Error('Invalid OpenAPI spec: missing paths');
|
|
@@ -570,18 +570,18 @@ function generateApiEnumsContent(enums) {
|
|
|
570
570
|
// Generate the generic enum helper utility
|
|
571
571
|
const helperUtility = `/**
|
|
572
572
|
* Generic utility for working with enums
|
|
573
|
-
*
|
|
573
|
+
*
|
|
574
574
|
* @example
|
|
575
575
|
* import { EnumHelper, RequestedValuationType } from './api-enums'
|
|
576
|
-
*
|
|
576
|
+
*
|
|
577
577
|
* // Get all values
|
|
578
578
|
* const allTypes = EnumHelper.values(RequestedValuationType)
|
|
579
|
-
*
|
|
579
|
+
*
|
|
580
580
|
* // Validate a value
|
|
581
581
|
* if (EnumHelper.isValid(RequestedValuationType, userInput)) {
|
|
582
582
|
* // TypeScript knows userInput is RequestedValuationType
|
|
583
583
|
* }
|
|
584
|
-
*
|
|
584
|
+
*
|
|
585
585
|
* // Reverse lookup
|
|
586
586
|
* const key = EnumHelper.getKey(RequestedValuationType, 'cat') // 'Cat'
|
|
587
587
|
*/
|
|
@@ -698,10 +698,10 @@ import type { components } from './openapi-types.js'
|
|
|
698
698
|
/**
|
|
699
699
|
* Type aliases for schema objects from the API spec.
|
|
700
700
|
* These are references to components['schemas'] for convenient importing.
|
|
701
|
-
*
|
|
701
|
+
*
|
|
702
702
|
* @example
|
|
703
703
|
* import type { Nuts, Address, BorrowerInfo } from './api-schemas'
|
|
704
|
-
*
|
|
704
|
+
*
|
|
705
705
|
* const nutsData: Nuts = { NUTS_ID: 'BE241', ... }
|
|
706
706
|
*/
|
|
707
707
|
`;
|
|
@@ -731,235 +731,451 @@ async function generateApiSchemas(openapiContent, outputDir, _excludePrefix = '_
|
|
|
731
731
|
console.log(`✅ Generated api-schemas file: ${outputPath}`);
|
|
732
732
|
console.log(`📊 Found ${schemaCount} schemas`);
|
|
733
733
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
734
|
+
// ============================================================================
|
|
735
|
+
// List path computation (ported from openapi-helpers.ts for code-gen time use)
|
|
736
|
+
// ============================================================================
|
|
737
|
+
const PLURAL_ES_SUFFIXES_CLI = ['s', 'x', 'z', 'ch', 'sh', 'o'];
|
|
738
|
+
function pluralizeResourceCli(name) {
|
|
739
|
+
if (name.endsWith('y'))
|
|
740
|
+
return name.slice(0, -1) + 'ies';
|
|
741
|
+
if (PLURAL_ES_SUFFIXES_CLI.some((s) => name.endsWith(s)))
|
|
742
|
+
return name + 'es';
|
|
743
|
+
return name + 's';
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Computes the list path for a mutation operation (used for cache invalidation).
|
|
747
|
+
* Returns null if no matching list operation is found.
|
|
748
|
+
*/
|
|
749
|
+
function computeListPath(operationId, opInfo, operationMap) {
|
|
750
|
+
const method = opInfo.method;
|
|
751
|
+
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
|
|
752
|
+
return null;
|
|
753
|
+
const prefixes = {
|
|
754
|
+
[HttpMethod.POST]: 'create',
|
|
755
|
+
[HttpMethod.PUT]: 'update',
|
|
756
|
+
[HttpMethod.PATCH]: 'update',
|
|
757
|
+
[HttpMethod.DELETE]: 'delete',
|
|
758
|
+
};
|
|
759
|
+
const prefix = prefixes[method];
|
|
760
|
+
if (!prefix)
|
|
761
|
+
return null;
|
|
762
|
+
let resourceName = null;
|
|
763
|
+
if (operationId.startsWith(prefix)) {
|
|
764
|
+
const remaining = operationId.slice(prefix.length);
|
|
765
|
+
if (remaining.length > 0 && /^[A-Z]/.test(remaining))
|
|
766
|
+
resourceName = remaining;
|
|
767
|
+
}
|
|
768
|
+
if (resourceName) {
|
|
769
|
+
const tryList = (name) => {
|
|
770
|
+
const listId = `list${name}`;
|
|
771
|
+
if (listId in operationMap && operationMap[listId].method === HttpMethod.GET)
|
|
772
|
+
return operationMap[listId].path;
|
|
773
|
+
return null;
|
|
774
|
+
};
|
|
775
|
+
const found = tryList(resourceName) || tryList(pluralizeResourceCli(resourceName));
|
|
776
|
+
if (found)
|
|
777
|
+
return found;
|
|
778
|
+
}
|
|
779
|
+
// Fallback: strip last path param segment
|
|
780
|
+
const segments = opInfo.path.split('/').filter((s) => s.length > 0);
|
|
781
|
+
if (segments.length >= 2 && /^\{[^}]+\}$/.test(segments[segments.length - 1])) {
|
|
782
|
+
return '/' + segments.slice(0, -1).join('/') + '/';
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
// ============================================================================
|
|
787
|
+
// New generator: api-client.ts
|
|
788
|
+
// ============================================================================
|
|
789
|
+
/**
|
|
790
|
+
* Generate JSDoc comment for an operation function.
|
|
791
|
+
*/
|
|
792
|
+
function _generateOperationJSDoc(operationId, method, apiPath) {
|
|
793
|
+
const methodUpper = method.toUpperCase();
|
|
794
|
+
const isQuery = ['GET', 'HEAD', 'OPTIONS'].includes(methodUpper);
|
|
795
|
+
const lines = ['/**', ` * ${operationId}`, ' * ', ` * ${methodUpper} ${apiPath}`];
|
|
796
|
+
if (isQuery) {
|
|
797
|
+
lines.push(' * ');
|
|
798
|
+
lines.push(' * @param pathParams - Path parameters (reactive)');
|
|
799
|
+
lines.push(' * @param options - Query options (enabled, staleTime, onLoad, etc.)');
|
|
800
|
+
lines.push(' * @returns Query result with data, isLoading, refetch(), etc.');
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
lines.push(' * ');
|
|
804
|
+
lines.push(' * @param pathParams - Path parameters (reactive)');
|
|
805
|
+
lines.push(' * @param options - Mutation options (onSuccess, onError, invalidateOperations, etc.)');
|
|
806
|
+
lines.push(' * @returns Mutation helper with mutate() and mutateAsync() methods');
|
|
807
|
+
}
|
|
808
|
+
lines.push(' */');
|
|
809
|
+
return lines.join('\n');
|
|
810
|
+
}
|
|
811
|
+
function generateApiClientContent(operationMap) {
|
|
812
|
+
const ids = Object.keys(operationMap).sort();
|
|
813
|
+
const QUERY_HTTP = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
814
|
+
const isQuery = (id) => QUERY_HTTP.has(operationMap[id].method);
|
|
815
|
+
const hasPathParams = (id) => operationMap[id].path.includes('{');
|
|
816
|
+
// Registry for invalidateOperations support
|
|
817
|
+
const registryEntries = ids.map((id) => ` ${id}: { path: '${operationMap[id].path}' },`).join('\n');
|
|
818
|
+
// Generic factory helpers (4 patterns)
|
|
819
|
+
const helpers = `/**
|
|
820
|
+
* Generic query helper for operations without path parameters.
|
|
821
|
+
* @internal
|
|
822
|
+
*/
|
|
823
|
+
function _queryNoParams<Op extends AllOps>(
|
|
824
|
+
base: _Config,
|
|
825
|
+
cfg: { path: string; method: HttpMethod; listPath: string | null },
|
|
826
|
+
enums: Record<string, unknown>,
|
|
827
|
+
) {
|
|
828
|
+
type Response = ApiResponse<Op>
|
|
829
|
+
type QueryParams = ApiQueryParams<Op>
|
|
767
830
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
831
|
+
const useQuery = (
|
|
832
|
+
options?: QueryOptions<Response, QueryParams>,
|
|
833
|
+
): QueryReturn<Response, Record<string, never>> =>
|
|
834
|
+
useEndpointQuery<Response, Record<string, never>, QueryParams>(
|
|
835
|
+
{ ...base, ...cfg },
|
|
836
|
+
undefined,
|
|
837
|
+
options,
|
|
838
|
+
)
|
|
776
839
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
840
|
+
return {
|
|
841
|
+
/**
|
|
842
|
+
* Query hook for this operation.
|
|
843
|
+
*
|
|
844
|
+
* Returns an object with:
|
|
845
|
+
* - \`data\`: The response data
|
|
846
|
+
* - \`isLoading\`: Whether the query is loading
|
|
847
|
+
* - \`error\`: Error object if the query failed
|
|
848
|
+
* - \`refetch\`: Function to manually trigger a refetch
|
|
849
|
+
* - \`isPending\`: Alias for isLoading
|
|
850
|
+
* - \`status\`: 'pending' | 'error' | 'success'
|
|
851
|
+
*
|
|
852
|
+
* @param options - Query options (enabled, refetchInterval, etc.)
|
|
853
|
+
* @returns Query result object
|
|
854
|
+
*/
|
|
855
|
+
useQuery,
|
|
856
|
+
enums,
|
|
857
|
+
} as const
|
|
786
858
|
}
|
|
787
859
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Generic query helper for operations with path parameters.
|
|
862
|
+
* @internal
|
|
863
|
+
*/
|
|
864
|
+
function _queryWithParams<Op extends AllOps>(
|
|
865
|
+
base: _Config,
|
|
866
|
+
cfg: { path: string; method: HttpMethod; listPath: string | null },
|
|
867
|
+
enums: Record<string, unknown>,
|
|
868
|
+
) {
|
|
869
|
+
type PathParams = ApiPathParams<Op>
|
|
870
|
+
type PathParamsInput = ApiPathParamsInput<Op>
|
|
871
|
+
type Response = ApiResponse<Op>
|
|
872
|
+
type QueryParams = ApiQueryParams<Op>
|
|
793
873
|
|
|
794
|
-
//
|
|
795
|
-
|
|
874
|
+
// Two-overload interface: non-function (exact via object-literal checking) +
|
|
875
|
+
// getter function (exact via NoExcessReturn constraint).
|
|
876
|
+
type _UseQuery = {
|
|
877
|
+
(
|
|
878
|
+
pathParams: PathParamsInput | Ref<PathParamsInput> | ComputedRef<PathParamsInput>,
|
|
879
|
+
options?: QueryOptions<Response, QueryParams>,
|
|
880
|
+
): QueryReturn<Response, PathParams>
|
|
881
|
+
<F extends () => PathParamsInput>(
|
|
882
|
+
pathParams: NoExcessReturn<PathParamsInput, F>,
|
|
883
|
+
options?: QueryOptions<Response, QueryParams>,
|
|
884
|
+
): QueryReturn<Response, PathParams>
|
|
885
|
+
}
|
|
796
886
|
|
|
797
|
-
|
|
887
|
+
const _impl = (
|
|
888
|
+
pathParams: ReactiveOr<PathParamsInput>,
|
|
889
|
+
options?: QueryOptions<Response, QueryParams>,
|
|
890
|
+
): QueryReturn<Response, PathParams> =>
|
|
891
|
+
useEndpointQuery<Response, PathParams, QueryParams>(
|
|
892
|
+
{ ...base, ...cfg },
|
|
893
|
+
pathParams as _PathParamsCast,
|
|
894
|
+
options,
|
|
895
|
+
)
|
|
798
896
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
897
|
+
return {
|
|
898
|
+
/**
|
|
899
|
+
* Query hook for this operation.
|
|
900
|
+
*
|
|
901
|
+
* Returns an object with:
|
|
902
|
+
* - \`data\`: The response data
|
|
903
|
+
* - \`isLoading\`: Whether the query is loading
|
|
904
|
+
* - \`error\`: Error object if the query failed
|
|
905
|
+
* - \`refetch\`: Function to manually trigger a refetch
|
|
906
|
+
* - \`isPending\`: Alias for isLoading
|
|
907
|
+
* - \`status\`: 'pending' | 'error' | 'success'
|
|
908
|
+
*
|
|
909
|
+
* @param pathParams - Path parameters (object, ref, computed, or getter function)
|
|
910
|
+
* @param options - Query options (enabled, refetchInterval, etc.)
|
|
911
|
+
* @returns Query result object
|
|
912
|
+
*/
|
|
913
|
+
useQuery: _impl as _UseQuery,
|
|
914
|
+
enums,
|
|
915
|
+
} as const
|
|
916
|
+
}
|
|
803
917
|
|
|
804
|
-
|
|
918
|
+
/**
|
|
919
|
+
* Generic mutation helper for operations without path parameters.
|
|
920
|
+
* @internal
|
|
921
|
+
*/
|
|
922
|
+
function _mutationNoParams<Op extends AllOps>(
|
|
923
|
+
base: _Config,
|
|
924
|
+
cfg: { path: string; method: HttpMethod; listPath: string | null },
|
|
925
|
+
enums: Record<string, unknown>,
|
|
926
|
+
) {
|
|
927
|
+
type RequestBody = ApiRequest<Op>
|
|
928
|
+
type Response = ApiResponse<Op>
|
|
929
|
+
type QueryParams = ApiQueryParams<Op>
|
|
805
930
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
931
|
+
const useMutation = (
|
|
932
|
+
options?: MutationOptions<Response, Record<string, never>, RequestBody, QueryParams>,
|
|
933
|
+
): MutationReturn<Response, Record<string, never>, RequestBody, QueryParams> =>
|
|
934
|
+
useEndpointMutation<Response, Record<string, never>, RequestBody, QueryParams>(
|
|
935
|
+
{ ...base, ...cfg },
|
|
936
|
+
undefined,
|
|
937
|
+
options,
|
|
938
|
+
)
|
|
810
939
|
|
|
811
|
-
|
|
940
|
+
return {
|
|
941
|
+
/**
|
|
942
|
+
* Mutation hook for this operation.
|
|
943
|
+
*
|
|
944
|
+
* Returns an object with:
|
|
945
|
+
* - \`mutate\`: Synchronous mutation function (returns void)
|
|
946
|
+
* - \`mutateAsync\`: Async mutation function (returns Promise)
|
|
947
|
+
* - \`data\`: The response data
|
|
948
|
+
* - \`isLoading\`: Whether the mutation is in progress
|
|
949
|
+
* - \`error\`: Error object if the mutation failed
|
|
950
|
+
* - \`isPending\`: Alias for isLoading
|
|
951
|
+
* - \`status\`: 'idle' | 'pending' | 'error' | 'success'
|
|
952
|
+
*
|
|
953
|
+
* @param options - Mutation options (onSuccess, onError, etc.)
|
|
954
|
+
* @returns Mutation result object
|
|
955
|
+
*/
|
|
956
|
+
useMutation,
|
|
957
|
+
enums,
|
|
958
|
+
} as const
|
|
959
|
+
}
|
|
812
960
|
|
|
813
961
|
/**
|
|
814
|
-
*
|
|
815
|
-
* Used for generic type constraints in helper types.
|
|
962
|
+
* Generic mutation helper for operations with path parameters.
|
|
816
963
|
* @internal
|
|
817
964
|
*/
|
|
818
|
-
|
|
965
|
+
function _mutationWithParams<Op extends AllOps>(
|
|
966
|
+
base: _Config,
|
|
967
|
+
cfg: { path: string; method: HttpMethod; listPath: string | null },
|
|
968
|
+
enums: Record<string, unknown>,
|
|
969
|
+
) {
|
|
970
|
+
type PathParams = ApiPathParams<Op>
|
|
971
|
+
type PathParamsInput = ApiPathParamsInput<Op>
|
|
972
|
+
type RequestBody = ApiRequest<Op>
|
|
973
|
+
type Response = ApiResponse<Op>
|
|
974
|
+
type QueryParams = ApiQueryParams<Op>
|
|
819
975
|
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
|
|
976
|
+
// Two-overload interface: non-function (exact via object-literal checking) +
|
|
977
|
+
// getter function (exact via NoExcessReturn constraint).
|
|
978
|
+
type _UseMutation = {
|
|
979
|
+
(
|
|
980
|
+
pathParams: PathParamsInput | Ref<PathParamsInput> | ComputedRef<PathParamsInput>,
|
|
981
|
+
options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
|
|
982
|
+
): MutationReturn<Response, PathParams, RequestBody, QueryParams>
|
|
983
|
+
<F extends () => PathParamsInput>(
|
|
984
|
+
pathParams: NoExcessReturn<PathParamsInput, F>,
|
|
985
|
+
options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
|
|
986
|
+
): MutationReturn<Response, PathParams, RequestBody, QueryParams>
|
|
987
|
+
}
|
|
823
988
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
989
|
+
const _impl = (
|
|
990
|
+
pathParams: ReactiveOr<PathParamsInput>,
|
|
991
|
+
options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
|
|
992
|
+
): MutationReturn<Response, PathParams, RequestBody, QueryParams> =>
|
|
993
|
+
useEndpointMutation<Response, PathParams, RequestBody, QueryParams>(
|
|
994
|
+
{ ...base, ...cfg },
|
|
995
|
+
pathParams as _PathParamsCast,
|
|
996
|
+
options,
|
|
997
|
+
)
|
|
831
998
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
999
|
+
return {
|
|
1000
|
+
/**
|
|
1001
|
+
* Mutation hook for this operation.
|
|
1002
|
+
*
|
|
1003
|
+
* Returns an object with:
|
|
1004
|
+
* - \`mutate\`: Synchronous mutation function (returns void)
|
|
1005
|
+
* - \`mutateAsync\`: Async mutation function (returns Promise)
|
|
1006
|
+
* - \`data\`: The response data
|
|
1007
|
+
* - \`isLoading\`: Whether the mutation is in progress
|
|
1008
|
+
* - \`error\`: Error object if the mutation failed
|
|
1009
|
+
* - \`isPending\`: Alias for isLoading
|
|
1010
|
+
* - \`status\`: 'idle' | 'pending' | 'error' | 'success'
|
|
1011
|
+
*
|
|
1012
|
+
* @param pathParams - Path parameters (object, ref, computed, or getter function)
|
|
1013
|
+
* @param options - Mutation options (onSuccess, onError, etc.)
|
|
1014
|
+
* @returns Mutation result object
|
|
1015
|
+
*/
|
|
1016
|
+
useMutation: _impl as _UseMutation,
|
|
1017
|
+
enums,
|
|
1018
|
+
} as const
|
|
1019
|
+
}`;
|
|
1020
|
+
// createApiClient factory with operation calls
|
|
1021
|
+
const factoryCalls = ids
|
|
1022
|
+
.map((id) => {
|
|
1023
|
+
const op = operationMap[id];
|
|
1024
|
+
const { path: apiPath, method } = op;
|
|
1025
|
+
const listPath = computeListPath(id, op, operationMap);
|
|
1026
|
+
const listPathStr = listPath ? `'${listPath}'` : 'null';
|
|
1027
|
+
const query = isQuery(id);
|
|
1028
|
+
const withParams = hasPathParams(id);
|
|
1029
|
+
const cfg = `{ path: '${apiPath}', method: HttpMethod.${method}, listPath: ${listPathStr} }`;
|
|
1030
|
+
const helper = query
|
|
1031
|
+
? withParams
|
|
1032
|
+
? '_queryWithParams'
|
|
1033
|
+
: '_queryNoParams'
|
|
1034
|
+
: withParams
|
|
1035
|
+
? '_mutationWithParams'
|
|
1036
|
+
: '_mutationNoParams';
|
|
1037
|
+
// Build JSDoc comment
|
|
1038
|
+
const docLines = [];
|
|
1039
|
+
// Summary/description
|
|
1040
|
+
if (op.summary) {
|
|
1041
|
+
docLines.push(op.summary);
|
|
1042
|
+
}
|
|
1043
|
+
if (op.description && op.description !== op.summary) {
|
|
1044
|
+
docLines.push(op.description);
|
|
1045
|
+
}
|
|
1046
|
+
// Path parameters
|
|
1047
|
+
if (op.pathParams && op.pathParams.length > 0) {
|
|
1048
|
+
const paramList = op.pathParams.map((p) => `${p.name}: ${p.type}`).join(', ');
|
|
1049
|
+
docLines.push(`@param pathParams - { ${paramList} }`);
|
|
1050
|
+
}
|
|
1051
|
+
// Request body
|
|
1052
|
+
if (op.requestBodySchema) {
|
|
1053
|
+
docLines.push(`@param body - Request body type: ${op.requestBodySchema}`);
|
|
1054
|
+
}
|
|
1055
|
+
// Response
|
|
1056
|
+
if (op.responseSchema) {
|
|
1057
|
+
docLines.push(`@returns Response type: ${op.responseSchema}`);
|
|
1058
|
+
}
|
|
1059
|
+
const jsDoc = docLines.length > 0 ? `\n /**\n * ${docLines.join('\n * ')}\n */` : '';
|
|
1060
|
+
return `${jsDoc}\n ${id}: ${helper}<'${id}'>(base, ${cfg}, ${id}_enums),`;
|
|
1061
|
+
})
|
|
1062
|
+
.join('');
|
|
1063
|
+
// Enum imports
|
|
1064
|
+
const enumImports = ids.map((id) => ` ${id}_enums,`).join('\n');
|
|
1065
|
+
// Type alias for AllOps
|
|
1066
|
+
const allOpsType = `type AllOps = keyof operations`;
|
|
1067
|
+
return `// Auto-generated from OpenAPI specification - do not edit manually
|
|
1068
|
+
// Use \`createApiClient\` to instantiate a fully-typed API client.
|
|
839
1069
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1070
|
+
import type { AxiosInstance } from 'axios'
|
|
1071
|
+
import type { Ref, ComputedRef } from 'vue'
|
|
1072
|
+
import {
|
|
1073
|
+
useEndpointQuery,
|
|
1074
|
+
useEndpointMutation,
|
|
1075
|
+
defaultQueryClient,
|
|
1076
|
+
HttpMethod,
|
|
1077
|
+
type QueryOptions,
|
|
1078
|
+
type MutationOptions,
|
|
1079
|
+
type QueryReturn,
|
|
1080
|
+
type MutationReturn,
|
|
1081
|
+
type ReactiveOr,
|
|
1082
|
+
type NoExcessReturn,
|
|
1083
|
+
type QueryClientLike,
|
|
1084
|
+
type MaybeRefOrGetter,
|
|
1085
|
+
} from '@qualisero/openapi-endpoint'
|
|
846
1086
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1087
|
+
import type {
|
|
1088
|
+
ApiResponse,
|
|
1089
|
+
ApiRequest,
|
|
1090
|
+
ApiPathParams,
|
|
1091
|
+
ApiPathParamsInput,
|
|
1092
|
+
ApiQueryParams,
|
|
1093
|
+
operations,
|
|
1094
|
+
} from './api-operations.js'
|
|
853
1095
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
* type Params = ApiQueryParams<OpType.listPets>
|
|
858
|
-
*/
|
|
859
|
-
export type ApiQueryParams<K extends AllOperationIds> = ApiQueryParamsBase<OpenApiOperations, K>
|
|
1096
|
+
import {
|
|
1097
|
+
${enumImports}
|
|
1098
|
+
} from './api-operations.js'
|
|
860
1099
|
|
|
861
1100
|
// ============================================================================
|
|
862
|
-
//
|
|
1101
|
+
// Operations registry (for invalidateOperations support)
|
|
863
1102
|
// ============================================================================
|
|
864
1103
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
*
|
|
869
|
-
* This is the idiomatic TypeScript pattern for enabling dot notation
|
|
870
|
-
* on type-level properties. The namespace is preferred over type aliases
|
|
871
|
-
* because it allows \`OpType.getPet\` instead of \`OpType['getPet']\`.
|
|
872
|
-
*
|
|
873
|
-
* @example
|
|
874
|
-
* type Response = ApiResponse<OpType.getPet>
|
|
875
|
-
* type Request = ApiRequest<OpType.createPet>
|
|
876
|
-
*/
|
|
877
|
-
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
878
|
-
export namespace OpType {
|
|
879
|
-
${opTypeContent}
|
|
880
|
-
}
|
|
1104
|
+
const _registry = {
|
|
1105
|
+
${registryEntries}
|
|
1106
|
+
} as const
|
|
881
1107
|
|
|
882
1108
|
// ============================================================================
|
|
883
|
-
//
|
|
1109
|
+
// Internal config type
|
|
884
1110
|
// ============================================================================
|
|
885
1111
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
*
|
|
891
|
-
* @example
|
|
892
|
-
* \`\`\`typescript
|
|
893
|
-
* const params: QueryParams['listPets'] = { limit: 10, status: 'available' }
|
|
894
|
-
* const query = api.useQuery(QueryOperationId.listPets, { queryParams: params })
|
|
895
|
-
* \`\`\`
|
|
896
|
-
*/
|
|
897
|
-
export type QueryParams = {
|
|
898
|
-
${queryParamsContent}
|
|
1112
|
+
type _Config = {
|
|
1113
|
+
axios: AxiosInstance
|
|
1114
|
+
queryClient: QueryClientLike
|
|
1115
|
+
operationsRegistry: typeof _registry
|
|
899
1116
|
}
|
|
900
1117
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
* Use this to get autocomplete on path parameter names and types.
|
|
905
|
-
*
|
|
906
|
-
* @example
|
|
907
|
-
* \`\`\`typescript
|
|
908
|
-
* const params: MutationParams['updatePet'] = { petId: '123' }
|
|
909
|
-
* const mutation = api.useMutation(MutationOperationId.updatePet, params)
|
|
910
|
-
* \`\`\`
|
|
911
|
-
*/
|
|
912
|
-
export type MutationParams = {
|
|
913
|
-
${mutationParamsContent}
|
|
914
|
-
}
|
|
1118
|
+
// ============================================================================
|
|
1119
|
+
// Type alias for path params cast (avoids repetition)
|
|
1120
|
+
// ============================================================================
|
|
915
1121
|
|
|
916
|
-
|
|
917
|
-
* Request body for each mutation operation.
|
|
918
|
-
*
|
|
919
|
-
* Use this to get autocomplete on request body properties and types.
|
|
920
|
-
*
|
|
921
|
-
* @example
|
|
922
|
-
* \`\`\`typescript
|
|
923
|
-
* const body: MutationBody['createPet'] = { name: 'Fluffy', species: 'cat' }
|
|
924
|
-
* await createPet.mutateAsync({ data: body })
|
|
925
|
-
* \`\`\`
|
|
926
|
-
*/
|
|
927
|
-
export type MutationBody = {
|
|
928
|
-
${mutationBodyContent}
|
|
929
|
-
}
|
|
1122
|
+
type _PathParamsCast = MaybeRefOrGetter<Record<string, string | number | undefined> | null | undefined>
|
|
930
1123
|
|
|
931
1124
|
// ============================================================================
|
|
932
|
-
//
|
|
1125
|
+
// Type alias for all operations
|
|
933
1126
|
// ============================================================================
|
|
934
|
-
// Use QueryOperationId or MutationOperationId directly for better type safety
|
|
935
1127
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1128
|
+
${allOpsType}
|
|
1129
|
+
|
|
1130
|
+
// ============================================================================
|
|
1131
|
+
// Shared generic factory helpers (4 patterns)
|
|
1132
|
+
// ============================================================================
|
|
1133
|
+
|
|
1134
|
+
${helpers}
|
|
1135
|
+
|
|
1136
|
+
// ============================================================================
|
|
1137
|
+
// Public API client factory
|
|
1138
|
+
// ============================================================================
|
|
941
1139
|
|
|
942
1140
|
/**
|
|
943
|
-
*
|
|
944
|
-
*
|
|
1141
|
+
* Create a fully-typed API client.
|
|
1142
|
+
*
|
|
1143
|
+
* Each operation in the spec is a property of the returned object:
|
|
1144
|
+
* - GET/HEAD/OPTIONS → \`api.opName.useQuery(...)\`
|
|
1145
|
+
* - POST/PUT/PATCH/DELETE → \`api.opName.useMutation(...)\`
|
|
1146
|
+
* - All operations → \`api.opName.enums.fieldName.Value\`
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* \`\`\`ts
|
|
1150
|
+
* import { createApiClient } from './generated/api-client'
|
|
1151
|
+
* import axios from 'axios'
|
|
1152
|
+
*
|
|
1153
|
+
* const api = createApiClient(axios.create({ baseURL: '/api' }))
|
|
1154
|
+
*
|
|
1155
|
+
* // In a Vue component:
|
|
1156
|
+
* const { data: pets } = api.listPets.useQuery()
|
|
1157
|
+
* const create = api.createPet.useMutation()
|
|
1158
|
+
* create.mutate({ data: { name: 'Fluffy' } })
|
|
1159
|
+
* \`\`\`
|
|
945
1160
|
*/
|
|
946
|
-
export
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
1161
|
+
export function createApiClient(axios: AxiosInstance, queryClient: QueryClientLike = defaultQueryClient) {
|
|
1162
|
+
const base: _Config = { axios, queryClient, operationsRegistry: _registry }
|
|
1163
|
+
return {
|
|
1164
|
+
${factoryCalls}
|
|
1165
|
+
} as const
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/** The fully-typed API client instance type. */
|
|
1169
|
+
export type ApiClient = ReturnType<typeof createApiClient>
|
|
950
1170
|
`;
|
|
951
1171
|
}
|
|
952
|
-
async function
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// Write to output file
|
|
958
|
-
const outputPath = path.join(outputDir, 'openapi-typed-operations.ts');
|
|
959
|
-
fs.writeFileSync(outputPath, tsContent);
|
|
960
|
-
console.log(`✅ Generated openapi-typed-operations file: ${outputPath}`);
|
|
961
|
-
console.log(`📊 Found ${operationIds.length} operations`);
|
|
1172
|
+
async function generateApiClientFile(openApiSpec, outputDir, excludePrefix) {
|
|
1173
|
+
const operationMap = buildOperationMap(openApiSpec, excludePrefix);
|
|
1174
|
+
const content = generateApiClientContent(operationMap);
|
|
1175
|
+
fs.writeFileSync(path.join(outputDir, 'api-client.ts'), content);
|
|
1176
|
+
console.log(`✅ Generated api-client.ts (${Object.keys(operationMap).length} operations)`);
|
|
962
1177
|
}
|
|
1178
|
+
// ============================================================================
|
|
963
1179
|
function printUsage() {
|
|
964
1180
|
console.log(`
|
|
965
1181
|
Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory> [options]
|
|
@@ -981,12 +1197,337 @@ Examples:
|
|
|
981
1197
|
npx @qualisero/openapi-endpoint ./api.json ./src/gen --no-exclude
|
|
982
1198
|
|
|
983
1199
|
This command will generate:
|
|
984
|
-
- openapi-types.ts
|
|
985
|
-
-
|
|
986
|
-
- api-
|
|
987
|
-
- api-
|
|
1200
|
+
- openapi-types.ts (TypeScript types from OpenAPI spec)
|
|
1201
|
+
- api-client.ts (Fully-typed createApiClient factory — main file to use)
|
|
1202
|
+
- api-operations.ts (Operations map + type aliases)
|
|
1203
|
+
- api-types.ts (Types namespace for type-only access)
|
|
1204
|
+
- api-enums.ts (Type-safe enum objects from OpenAPI spec)
|
|
1205
|
+
- api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
|
|
988
1206
|
`);
|
|
989
1207
|
}
|
|
1208
|
+
// ============================================================================
|
|
1209
|
+
// New helper functions for operation-named API
|
|
1210
|
+
// ============================================================================
|
|
1211
|
+
/**
|
|
1212
|
+
* Parses an already-loaded OpenAPISpec into a map of operationId → OperationInfo.
|
|
1213
|
+
* @param openApiSpec The parsed OpenAPI spec object
|
|
1214
|
+
* @param excludePrefix Operations with this prefix are excluded
|
|
1215
|
+
* @returns Map of operation ID to { path, method }
|
|
1216
|
+
*/
|
|
1217
|
+
function buildOperationMap(openApiSpec, excludePrefix) {
|
|
1218
|
+
const map = {};
|
|
1219
|
+
for (const [pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
|
|
1220
|
+
for (const [method, rawOp] of Object.entries(pathItem)) {
|
|
1221
|
+
if (!HTTP_METHODS.includes(method))
|
|
1222
|
+
continue;
|
|
1223
|
+
const op = rawOp;
|
|
1224
|
+
if (!op.operationId)
|
|
1225
|
+
continue;
|
|
1226
|
+
if (excludePrefix && op.operationId.startsWith(excludePrefix))
|
|
1227
|
+
continue;
|
|
1228
|
+
// Extract path and query parameters
|
|
1229
|
+
const pathParams = [];
|
|
1230
|
+
const queryParams = [];
|
|
1231
|
+
if (op.parameters) {
|
|
1232
|
+
for (const param of op.parameters) {
|
|
1233
|
+
const type = param.schema?.type || 'string';
|
|
1234
|
+
if (param.in === 'path') {
|
|
1235
|
+
pathParams.push({ name: param.name, type });
|
|
1236
|
+
}
|
|
1237
|
+
else if (param.in === 'query') {
|
|
1238
|
+
queryParams.push({ name: param.name, type });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Extract request body schema
|
|
1243
|
+
let requestBodySchema;
|
|
1244
|
+
const reqBodyRef = op.requestBody?.content?.['application/json']?.schema?.$ref;
|
|
1245
|
+
if (reqBodyRef) {
|
|
1246
|
+
requestBodySchema = reqBodyRef.split('/').pop();
|
|
1247
|
+
}
|
|
1248
|
+
// Extract response schema (from 200/201 responses)
|
|
1249
|
+
let responseSchema;
|
|
1250
|
+
if (op.responses) {
|
|
1251
|
+
for (const statusCode of ['200', '201']) {
|
|
1252
|
+
const resRef = op.responses[statusCode]?.content?.['application/json']?.schema?.$ref;
|
|
1253
|
+
if (resRef) {
|
|
1254
|
+
responseSchema = resRef.split('/').pop();
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
map[op.operationId] = {
|
|
1260
|
+
path: pathUrl,
|
|
1261
|
+
method: method.toUpperCase(),
|
|
1262
|
+
summary: op.summary,
|
|
1263
|
+
description: op.description,
|
|
1264
|
+
pathParams: pathParams.length > 0 ? pathParams : undefined,
|
|
1265
|
+
queryParams: queryParams.length > 0 ? queryParams : undefined,
|
|
1266
|
+
requestBodySchema,
|
|
1267
|
+
responseSchema,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return map;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Converts an OpenAPI enum array to `{ MemberName: 'value' }`.
|
|
1275
|
+
* @param values Enum values (may include null)
|
|
1276
|
+
* @returns Object with PascalCase keys and string literal values
|
|
1277
|
+
*/
|
|
1278
|
+
function enumArrayToObject(values) {
|
|
1279
|
+
const obj = {};
|
|
1280
|
+
for (const v of values) {
|
|
1281
|
+
if (v === null)
|
|
1282
|
+
continue;
|
|
1283
|
+
obj[toEnumMemberName(v)] = String(v);
|
|
1284
|
+
}
|
|
1285
|
+
return obj;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* For each operation, extract enum fields from:
|
|
1289
|
+
* 1. Request body object properties (direct `enum` or `$ref` to an enum schema)
|
|
1290
|
+
* 2. Query and path parameters with `enum`
|
|
1291
|
+
* @param openApiSpec The parsed OpenAPI spec
|
|
1292
|
+
* @param operationMap Map from buildOperationMap
|
|
1293
|
+
* @returns operationId → { fieldName → { MemberName: 'value' } }
|
|
1294
|
+
*/
|
|
1295
|
+
function buildOperationEnums(openApiSpec, operationMap) {
|
|
1296
|
+
// Schema-level enum lookup: schemaName → { MemberName: value }
|
|
1297
|
+
const schemaEnumLookup = {};
|
|
1298
|
+
if (openApiSpec.components?.schemas) {
|
|
1299
|
+
for (const [schemaName, schema] of Object.entries(openApiSpec.components.schemas)) {
|
|
1300
|
+
if (schema.enum) {
|
|
1301
|
+
schemaEnumLookup[schemaName] = enumArrayToObject(schema.enum);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function resolveEnums(schema) {
|
|
1306
|
+
if (schema.enum)
|
|
1307
|
+
return enumArrayToObject(schema.enum);
|
|
1308
|
+
if (typeof schema.$ref === 'string') {
|
|
1309
|
+
const name = schema.$ref.split('/').pop();
|
|
1310
|
+
return schemaEnumLookup[name] ?? null;
|
|
1311
|
+
}
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
const result = {};
|
|
1315
|
+
for (const [_pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
|
|
1316
|
+
for (const [method, rawOp] of Object.entries(pathItem)) {
|
|
1317
|
+
if (!HTTP_METHODS.includes(method))
|
|
1318
|
+
continue;
|
|
1319
|
+
const op = rawOp;
|
|
1320
|
+
if (!op.operationId || !(op.operationId in operationMap))
|
|
1321
|
+
continue;
|
|
1322
|
+
const fields = {};
|
|
1323
|
+
// Request body properties
|
|
1324
|
+
const bodyProps = op.requestBody?.content?.['application/json']?.schema?.properties;
|
|
1325
|
+
if (bodyProps) {
|
|
1326
|
+
for (const [fieldName, fieldSchema] of Object.entries(bodyProps)) {
|
|
1327
|
+
const resolved = resolveEnums(fieldSchema);
|
|
1328
|
+
if (resolved)
|
|
1329
|
+
fields[fieldName] = resolved;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
// Query + path parameters
|
|
1333
|
+
for (const param of op.parameters ?? []) {
|
|
1334
|
+
if (param.schema) {
|
|
1335
|
+
const resolved = resolveEnums(param.schema);
|
|
1336
|
+
if (resolved)
|
|
1337
|
+
fields[param.name] = resolved;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
result[op.operationId] = fields;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
// ============================================================================
|
|
1346
|
+
// New generators: api-operations.ts
|
|
1347
|
+
// ============================================================================
|
|
1348
|
+
/**
|
|
1349
|
+
* Generates the content for `api-operations.ts` file.
|
|
1350
|
+
* @param operationMap The operation info map
|
|
1351
|
+
* @param opEnums Per-operation enum fields
|
|
1352
|
+
* @param schemaEnumNames Names from api-enums.ts to re-export
|
|
1353
|
+
* @returns Generated TypeScript file content
|
|
1354
|
+
*/
|
|
1355
|
+
function generateApiOperationsContent(operationMap, opEnums, schemaEnumNames) {
|
|
1356
|
+
const ids = Object.keys(operationMap).sort();
|
|
1357
|
+
const _queryIds = ids.filter((id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method));
|
|
1358
|
+
const _mutationIds = ids.filter((id) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(operationMap[id].method));
|
|
1359
|
+
// Per-operation enum consts
|
|
1360
|
+
const enumConsts = ids
|
|
1361
|
+
.map((id) => {
|
|
1362
|
+
const fields = opEnums[id] ?? {};
|
|
1363
|
+
const body = Object.entries(fields)
|
|
1364
|
+
.map(([field, vals]) => {
|
|
1365
|
+
const members = Object.entries(vals)
|
|
1366
|
+
.map(([k, v]) => ` ${k}: ${JSON.stringify(v)} as const,`)
|
|
1367
|
+
.join('\n');
|
|
1368
|
+
return ` ${field}: {\n${members}\n } as const,`;
|
|
1369
|
+
})
|
|
1370
|
+
.join('\n');
|
|
1371
|
+
return `export const ${id}_enums = {\n${body}\n} as const`;
|
|
1372
|
+
})
|
|
1373
|
+
.join('\n\n');
|
|
1374
|
+
// Operations map
|
|
1375
|
+
const opEntries = ids
|
|
1376
|
+
.map((id) => ` ${id}: { path: '${operationMap[id].path}', method: HttpMethod.${operationMap[id].method} },`)
|
|
1377
|
+
.join('\n');
|
|
1378
|
+
// Type helpers — now use openapi-typescript `operations` directly (not OpenApiOperations)
|
|
1379
|
+
const typeHelpers = `
|
|
1380
|
+
type AllOps = keyof operations
|
|
1381
|
+
|
|
1382
|
+
/** Response data type for an operation (all fields required). */
|
|
1383
|
+
export type ApiResponse<K extends AllOps> = _ApiResponse<operations, K>
|
|
1384
|
+
/** Response data type - only \`readonly\` fields required. */
|
|
1385
|
+
export type ApiResponseSafe<K extends AllOps> = _ApiResponseSafe<operations, K>
|
|
1386
|
+
/** Request body type. */
|
|
1387
|
+
export type ApiRequest<K extends AllOps> = _ApiRequest<operations, K>
|
|
1388
|
+
/** Path parameters type. */
|
|
1389
|
+
export type ApiPathParams<K extends AllOps> = _ApiPathParams<operations, K>
|
|
1390
|
+
/** Path parameters input type (allows undefined values for reactive resolution). */
|
|
1391
|
+
export type ApiPathParamsInput<K extends AllOps> = _ApiPathParamsInput<operations, K>
|
|
1392
|
+
/** Query parameters type. */
|
|
1393
|
+
export type ApiQueryParams<K extends AllOps> = _ApiQueryParams<operations, K>`;
|
|
1394
|
+
// Re-exports
|
|
1395
|
+
// Use type-only wildcard export to avoid duplicate identifier errors
|
|
1396
|
+
const reExports = schemaEnumNames.length > 0
|
|
1397
|
+
? schemaEnumNames.map((n) => `export { ${n} } from './api-enums'`).join('\n') +
|
|
1398
|
+
"\nexport type * from './api-enums'"
|
|
1399
|
+
: '// No schema-level enums to re-export';
|
|
1400
|
+
return `// Auto-generated from OpenAPI specification - do not edit manually
|
|
1401
|
+
|
|
1402
|
+
import type { operations } from './openapi-types.js'
|
|
1403
|
+
import { HttpMethod } from '@qualisero/openapi-endpoint'
|
|
1404
|
+
import type {
|
|
1405
|
+
ApiResponse as _ApiResponse,
|
|
1406
|
+
ApiResponseSafe as _ApiResponseSafe,
|
|
1407
|
+
ApiRequest as _ApiRequest,
|
|
1408
|
+
ApiPathParams as _ApiPathParams,
|
|
1409
|
+
ApiPathParamsInput as _ApiPathParamsInput,
|
|
1410
|
+
ApiQueryParams as _ApiQueryParams,
|
|
1411
|
+
} from '@qualisero/openapi-endpoint'
|
|
1412
|
+
|
|
1413
|
+
export type { operations }
|
|
1414
|
+
|
|
1415
|
+
${reExports}
|
|
1416
|
+
|
|
1417
|
+
// ============================================================================
|
|
1418
|
+
// Per-operation enum values
|
|
1419
|
+
// ============================================================================
|
|
1420
|
+
|
|
1421
|
+
${enumConsts}
|
|
1422
|
+
|
|
1423
|
+
// ============================================================================
|
|
1424
|
+
// Operations map (kept for inspection / backward compatibility)
|
|
1425
|
+
// ============================================================================
|
|
1426
|
+
|
|
1427
|
+
const operationsBase = {
|
|
1428
|
+
${opEntries}
|
|
1429
|
+
} as const
|
|
1430
|
+
|
|
1431
|
+
export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
|
|
1432
|
+
export type OpenApiOperations = typeof openApiOperations
|
|
1433
|
+
|
|
1434
|
+
// ============================================================================
|
|
1435
|
+
// Convenience type aliases
|
|
1436
|
+
// ============================================================================
|
|
1437
|
+
${typeHelpers}
|
|
1438
|
+
`;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Async wrapper for generateApiOperationsContent.
|
|
1442
|
+
*/
|
|
1443
|
+
async function generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames) {
|
|
1444
|
+
console.log('🔨 Generating api-operations.ts...');
|
|
1445
|
+
const operationMap = buildOperationMap(openApiSpec, excludePrefix);
|
|
1446
|
+
const opEnums = buildOperationEnums(openApiSpec, operationMap);
|
|
1447
|
+
const content = generateApiOperationsContent(operationMap, opEnums, schemaEnumNames);
|
|
1448
|
+
fs.writeFileSync(path.join(outputDir, 'api-operations.ts'), content);
|
|
1449
|
+
console.log(`✅ Generated api-operations.ts (${Object.keys(operationMap).length} operations)`);
|
|
1450
|
+
}
|
|
1451
|
+
// ============================================================================
|
|
1452
|
+
// New generators: api-types.ts
|
|
1453
|
+
// ============================================================================
|
|
1454
|
+
/**
|
|
1455
|
+
* Generates the content for `api-types.ts` file.
|
|
1456
|
+
* @param operationMap The operation info map
|
|
1457
|
+
* @param opEnums Per-operation enum fields
|
|
1458
|
+
* @returns Generated TypeScript file content
|
|
1459
|
+
*/
|
|
1460
|
+
function generateApiTypesContent(operationMap, opEnums) {
|
|
1461
|
+
const ids = Object.keys(operationMap).sort();
|
|
1462
|
+
const isQuery = (id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method);
|
|
1463
|
+
const namespaces = ids
|
|
1464
|
+
.map((id) => {
|
|
1465
|
+
const query = isQuery(id);
|
|
1466
|
+
const fields = opEnums[id] ?? {};
|
|
1467
|
+
const enumTypes = Object.entries(fields)
|
|
1468
|
+
.map(([fieldName, vals]) => {
|
|
1469
|
+
const typeName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
|
|
1470
|
+
const union = Object.values(vals)
|
|
1471
|
+
.map((v) => `'${v}'`)
|
|
1472
|
+
.join(' | ');
|
|
1473
|
+
return ` /** \`${union}\` */\n export type ${typeName} = ${union}`;
|
|
1474
|
+
})
|
|
1475
|
+
.join('\n');
|
|
1476
|
+
const commonLines = [
|
|
1477
|
+
` /** Full response type - all fields required. */`,
|
|
1478
|
+
` export type Response = _ApiResponse<OpenApiOperations, '${id}'>`,
|
|
1479
|
+
` /** Response type - only \`readonly\` fields required. */`,
|
|
1480
|
+
` export type SafeResponse = _ApiResponseSafe<OpenApiOperations, '${id}'>`,
|
|
1481
|
+
];
|
|
1482
|
+
if (!query) {
|
|
1483
|
+
commonLines.push(` /** Request body type. */`, ` export type Request = _ApiRequest<OpenApiOperations, '${id}'>`);
|
|
1484
|
+
}
|
|
1485
|
+
commonLines.push(` /** Path parameters. */`, ` export type PathParams = _ApiPathParams<OpenApiOperations, '${id}'>`, ` /** Query parameters. */`, ` export type QueryParams = _ApiQueryParams<OpenApiOperations, '${id}'>`);
|
|
1486
|
+
const enumNs = enumTypes ? ` export namespace Enums {\n${enumTypes}\n }` : ` export namespace Enums {}`;
|
|
1487
|
+
return ` export namespace ${id} {\n${commonLines.join('\n')}\n${enumNs}\n }`;
|
|
1488
|
+
})
|
|
1489
|
+
.join('\n\n');
|
|
1490
|
+
return `/* eslint-disable */
|
|
1491
|
+
// Auto-generated from OpenAPI specification — do not edit manually
|
|
1492
|
+
|
|
1493
|
+
import type {
|
|
1494
|
+
ApiResponse as _ApiResponse,
|
|
1495
|
+
ApiResponseSafe as _ApiResponseSafe,
|
|
1496
|
+
ApiRequest as _ApiRequest,
|
|
1497
|
+
ApiPathParams as _ApiPathParams,
|
|
1498
|
+
ApiQueryParams as _ApiQueryParams,
|
|
1499
|
+
} from '@qualisero/openapi-endpoint'
|
|
1500
|
+
import type { operations as OpenApiOperations } from './openapi-types.js'
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Type-only namespace for all API operations.
|
|
1504
|
+
*
|
|
1505
|
+
* @example
|
|
1506
|
+
* \`\`\`ts
|
|
1507
|
+
* import type { Types } from './generated/api-types'
|
|
1508
|
+
*
|
|
1509
|
+
* type Pet = Types.getPet.Response
|
|
1510
|
+
* type NewPet = Types.createPet.Request
|
|
1511
|
+
* type PetStatus = Types.createPet.Enums.Status // 'available' | 'pending' | 'adopted'
|
|
1512
|
+
* type Params = Types.getPet.PathParams // { petId: string }
|
|
1513
|
+
* \`\`\`
|
|
1514
|
+
*/
|
|
1515
|
+
export namespace Types {
|
|
1516
|
+
${namespaces}
|
|
1517
|
+
}
|
|
1518
|
+
`;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Async wrapper for generateApiTypesContent.
|
|
1522
|
+
*/
|
|
1523
|
+
async function generateApiTypesFile(openApiSpec, outputDir, excludePrefix) {
|
|
1524
|
+
console.log('🔨 Generating api-types.ts...');
|
|
1525
|
+
const operationMap = buildOperationMap(openApiSpec, excludePrefix);
|
|
1526
|
+
const opEnums = buildOperationEnums(openApiSpec, operationMap);
|
|
1527
|
+
const content = generateApiTypesContent(operationMap, opEnums);
|
|
1528
|
+
fs.writeFileSync(path.join(outputDir, 'api-types.ts'), content);
|
|
1529
|
+
console.log(`✅ Generated api-types.ts`);
|
|
1530
|
+
}
|
|
990
1531
|
async function main() {
|
|
991
1532
|
const args = process.argv.slice(2);
|
|
992
1533
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
@@ -1030,18 +1571,22 @@ async function main() {
|
|
|
1030
1571
|
else {
|
|
1031
1572
|
console.log(`✅ Including all operations (no exclusion filter)`);
|
|
1032
1573
|
}
|
|
1033
|
-
// Fetch OpenAPI spec
|
|
1574
|
+
// Fetch and parse OpenAPI spec once
|
|
1034
1575
|
let openapiContent = await fetchOpenAPISpec(openapiInput);
|
|
1035
|
-
// Parse spec and add missing operationIds
|
|
1036
1576
|
const openApiSpec = JSON.parse(openapiContent);
|
|
1577
|
+
// Add missing operationIds
|
|
1037
1578
|
addMissingOperationIds(openApiSpec);
|
|
1038
1579
|
openapiContent = JSON.stringify(openApiSpec, null, 2);
|
|
1580
|
+
// Collect schema enum names for re-export
|
|
1581
|
+
const schemaEnumNames = extractEnumsFromSpec(openApiSpec).map((e) => e.name);
|
|
1039
1582
|
// Generate all files
|
|
1040
1583
|
await Promise.all([
|
|
1041
1584
|
generateTypes(openapiContent, outputDir),
|
|
1042
|
-
generateApiOperations(openapiContent, outputDir, excludePrefix),
|
|
1043
1585
|
generateApiEnums(openapiContent, outputDir, excludePrefix),
|
|
1044
1586
|
generateApiSchemas(openapiContent, outputDir, excludePrefix),
|
|
1587
|
+
generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames),
|
|
1588
|
+
generateApiTypesFile(openApiSpec, outputDir, excludePrefix),
|
|
1589
|
+
generateApiClientFile(openApiSpec, outputDir, excludePrefix),
|
|
1045
1590
|
]);
|
|
1046
1591
|
console.log('🎉 Code generation completed successfully!');
|
|
1047
1592
|
}
|