@qualisero/openapi-endpoint 0.13.2 → 0.15.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/dist/cli.js CHANGED
@@ -203,7 +203,7 @@ function addMissingOperationIds(openApiSpec, prefixToStrip = '/api') {
203
203
  });
204
204
  });
205
205
  }
206
- function parseOperationsFromSpec(openapiContent, excludePrefix = '_deprecated') {
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,452 @@ 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
- function generateApiOperationsContent(operationIds, operationInfoMap) {
735
- // Generate operationsBase dictionary
736
- const operationsBaseContent = operationIds
737
- .map((id) => {
738
- const info = operationInfoMap[id];
739
- return ` ${id}: {\n path: '${info.path}',\n method: HttpMethod.${info.method},\n },`;
740
- })
741
- .join('\n');
742
- const queryOperationIds = operationIds.filter((id) => {
743
- const method = operationInfoMap[id]?.method;
744
- return method === HttpMethod.GET || method === HttpMethod.HEAD || method === HttpMethod.OPTIONS;
745
- });
746
- const mutationOperationIds = operationIds.filter((id) => {
747
- const method = operationInfoMap[id]?.method;
748
- return (method === HttpMethod.POST ||
749
- method === HttpMethod.PUT ||
750
- method === HttpMethod.PATCH ||
751
- method === HttpMethod.DELETE);
752
- });
753
- // Generate filtered OperationId enums (source of truth)
754
- const queryOperationIdContent = queryOperationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
755
- const mutationOperationIdContent = mutationOperationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
756
- // Generate OpType namespace from BOTH lists
757
- const opTypeContent = [
758
- ...queryOperationIds.map((id) => ` export type ${id} = typeof QueryOperationId.${id}`),
759
- ...mutationOperationIds.map((id) => ` export type ${id} = typeof MutationOperationId.${id}`),
760
- ].join('\n');
761
- // Generate pre-computed type alias content for Phase 3B
762
- const queryParamsContent = queryOperationIds.map((id) => ` ${id}: ApiQueryParams<OpType.${id}>`).join('\n');
763
- const mutationParamsContent = mutationOperationIds.map((id) => ` ${id}: ApiPathParams<OpType.${id}>`).join('\n');
764
- const mutationBodyContent = mutationOperationIds.map((id) => ` ${id}: ApiRequest<OpType.${id}>`).join('\n');
765
- return `// Auto-generated from OpenAPI specification
766
- // Do not edit this file manually
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
- import type { operations } from './openapi-types.js'
769
- import type {
770
- ApiResponse as ApiResponseBase,
771
- ApiResponseSafe as ApiResponseSafeBase,
772
- ApiRequest as ApiRequestBase,
773
- ApiPathParams as ApiPathParamsBase,
774
- ApiQueryParams as ApiQueryParamsBase,
775
- } from '@qualisero/openapi-endpoint'
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
- export enum HttpMethod {
778
- GET = 'GET',
779
- POST = 'POST',
780
- PUT = 'PUT',
781
- PATCH = 'PATCH',
782
- DELETE = 'DELETE',
783
- HEAD = 'HEAD',
784
- OPTIONS = 'OPTIONS',
785
- TRACE = 'TRACE',
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
- // Create the typed structure that combines operations with operation metadata
789
- // This ensures the debug method returns correct values and all operations are properly typed
790
- const operationsBase = {
791
- ${operationsBaseContent}
792
- } as const
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
- // Merge with operations type to maintain OpenAPI type information
795
- export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
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
- export type OpenApiOperations = typeof openApiOperations
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
- // Query operations only - use with useQuery() for better autocomplete
800
- export const QueryOperationId = {
801
- ${queryOperationIdContent}
802
- } satisfies Partial<Record<keyof typeof operationsBase, keyof typeof operationsBase>>
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
- export type QueryOperationId = keyof typeof QueryOperationId
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
- // Mutation operations only - use with useMutation() for better autocomplete
807
- export const MutationOperationId = {
808
- ${mutationOperationIdContent}
809
- } satisfies Partial<Record<keyof typeof operationsBase, keyof typeof operationsBase>>
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
- export type MutationOperationId = keyof typeof MutationOperationId
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
- * Union type of all operation IDs (queries and mutations).
815
- * Used for generic type constraints in helper types.
962
+ * Generic mutation helper for operations with path parameters.
816
963
  * @internal
817
964
  */
818
- export type AllOperationIds = QueryOperationId | MutationOperationId
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
- // Type-safe API Helpers - Use OpType.XXX for type-safe access with intellisense
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
- * Response data type for an API operation.
826
- * All fields are REQUIRED - no null checks needed.
827
- * @example
828
- * type Response = ApiResponse<OpType.getPet>
829
- */
830
- export type ApiResponse<K extends AllOperationIds> = ApiResponseBase<OpenApiOperations, K>
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
- * Response data type with safe typing for unreliable backends.
834
- * Only readonly properties are required; others may be undefined.
835
- * @example
836
- * type Response = ApiResponseSafe<OpType.getPet>
837
- */
838
- export type ApiResponseSafe<K extends AllOperationIds> = ApiResponseSafeBase<OpenApiOperations, K>
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
- * Request body type for a mutation operation.
842
- * @example
843
- * type Request = ApiRequest<OpType.createPet>
844
- */
845
- export type ApiRequest<K extends AllOperationIds> = ApiRequestBase<OpenApiOperations, K>
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 MaybeRefOrGetter,
1084
+ } from '@qualisero/openapi-endpoint'
846
1085
 
847
- /**
848
- * Path parameters type for an operation.
849
- * @example
850
- * type Params = ApiPathParams<OpType.getPet>
851
- */
852
- export type ApiPathParams<K extends AllOperationIds> = ApiPathParamsBase<OpenApiOperations, K>
1086
+ import type { QueryClient } from '@tanstack/vue-query'
853
1087
 
854
- /**
855
- * Query parameters type for an operation.
856
- * @example
857
- * type Params = ApiQueryParams<OpType.listPets>
858
- */
859
- export type ApiQueryParams<K extends AllOperationIds> = ApiQueryParamsBase<OpenApiOperations, K>
1088
+ import type {
1089
+ ApiResponse,
1090
+ ApiRequest,
1091
+ ApiPathParams,
1092
+ ApiPathParamsInput,
1093
+ ApiQueryParams,
1094
+ operations,
1095
+ } from './api-operations.js'
1096
+
1097
+ import {
1098
+ ${enumImports}
1099
+ } from './api-operations.js'
860
1100
 
861
1101
  // ============================================================================
862
- // OpType namespace - enables dot notation: ApiResponse<OpType.getPet>
1102
+ // Operations registry (for invalidateOperations support)
863
1103
  // ============================================================================
864
1104
 
865
- /**
866
- * Namespace that mirrors operation IDs as types.
867
- * Enables dot notation syntax: ApiResponse<OpType.getPet>
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
- }
1105
+ const _registry = {
1106
+ ${registryEntries}
1107
+ } as const
881
1108
 
882
1109
  // ============================================================================
883
- // Pre-Computed Type Aliases - For easier DX and clearer intent
1110
+ // Internal config type
884
1111
  // ============================================================================
885
1112
 
886
- /**
887
- * Query parameters for each query operation.
888
- *
889
- * Use this to get autocomplete on query parameter names and types.
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}
1113
+ type _Config = {
1114
+ axios: AxiosInstance
1115
+ queryClient: QueryClient
1116
+ operationsRegistry: typeof _registry
899
1117
  }
900
1118
 
901
- /**
902
- * Path parameters for each mutation operation.
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
- }
1119
+ // ============================================================================
1120
+ // Type alias for path params cast (avoids repetition)
1121
+ // ============================================================================
915
1122
 
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
- }
1123
+ type _PathParamsCast = MaybeRefOrGetter<Record<string, string | number | undefined> | null | undefined>
930
1124
 
931
1125
  // ============================================================================
932
- // LEGACY: OperationId (auto-derived from union for backward compatibility)
1126
+ // Type alias for all operations
933
1127
  // ============================================================================
934
- // Use QueryOperationId or MutationOperationId directly for better type safety
935
1128
 
936
- /**
937
- * @deprecated Use QueryOperationId or MutationOperationId instead.
938
- * Auto-derived from their union for backward compatibility.
939
- */
940
- export type OperationId = AllOperationIds
1129
+ ${allOpsType}
1130
+
1131
+ // ============================================================================
1132
+ // Shared generic factory helpers (4 patterns)
1133
+ // ============================================================================
1134
+
1135
+ ${helpers}
1136
+
1137
+ // ============================================================================
1138
+ // Public API client factory
1139
+ // ============================================================================
941
1140
 
942
1141
  /**
943
- * @deprecated Use QueryOperationId or MutationOperationId instead.
944
- * Auto-derived from their union for backward compatibility.
1142
+ * Create a fully-typed API client.
1143
+ *
1144
+ * Each operation in the spec is a property of the returned object:
1145
+ * - GET/HEAD/OPTIONS → \`api.opName.useQuery(...)\`
1146
+ * - POST/PUT/PATCH/DELETE → \`api.opName.useMutation(...)\`
1147
+ * - All operations → \`api.opName.enums.fieldName.Value\`
1148
+ *
1149
+ * @example
1150
+ * \`\`\`ts
1151
+ * import { createApiClient } from './generated/api-client'
1152
+ * import axios from 'axios'
1153
+ *
1154
+ * const api = createApiClient(axios.create({ baseURL: '/api' }))
1155
+ *
1156
+ * // In a Vue component:
1157
+ * const { data: pets } = api.listPets.useQuery()
1158
+ * const create = api.createPet.useMutation()
1159
+ * create.mutate({ data: { name: 'Fluffy' } })
1160
+ * \`\`\`
945
1161
  */
946
- export const OperationId = {
947
- ...QueryOperationId,
948
- ...MutationOperationId,
949
- } satisfies Record<AllOperationIds, AllOperationIds>
1162
+ export function createApiClient(axios: AxiosInstance, queryClient: QueryClient = defaultQueryClient) {
1163
+ const base: _Config = { axios, queryClient, operationsRegistry: _registry }
1164
+ return {
1165
+ ${factoryCalls}
1166
+ } as const
1167
+ }
1168
+
1169
+ /** The fully-typed API client instance type. */
1170
+ export type ApiClient = ReturnType<typeof createApiClient>
950
1171
  `;
951
1172
  }
952
- async function generateApiOperations(openapiContent, outputDir, excludePrefix = '_deprecated') {
953
- console.log('🔨 Generating openapi-typed-operations.ts file...');
954
- const { operationIds, operationInfoMap } = parseOperationsFromSpec(openapiContent, excludePrefix);
955
- // Generate TypeScript content
956
- const tsContent = generateApiOperationsContent(operationIds, operationInfoMap);
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`);
1173
+ async function generateApiClientFile(openApiSpec, outputDir, excludePrefix) {
1174
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1175
+ const content = generateApiClientContent(operationMap);
1176
+ fs.writeFileSync(path.join(outputDir, 'api-client.ts'), content);
1177
+ console.log(`✅ Generated api-client.ts (${Object.keys(operationMap).length} operations)`);
962
1178
  }
1179
+ // ============================================================================
963
1180
  function printUsage() {
964
1181
  console.log(`
965
1182
  Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory> [options]
@@ -981,12 +1198,337 @@ Examples:
981
1198
  npx @qualisero/openapi-endpoint ./api.json ./src/gen --no-exclude
982
1199
 
983
1200
  This command will generate:
984
- - openapi-types.ts (TypeScript types from OpenAPI spec)
985
- - openapi-typed-operations.ts (Operation IDs and info for use with this library)
986
- - api-enums.ts (Type-safe enum objects from OpenAPI spec)
987
- - api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
1201
+ - openapi-types.ts (TypeScript types from OpenAPI spec)
1202
+ - api-client.ts (Fully-typed createApiClient factory main file to use)
1203
+ - api-operations.ts (Operations map + type aliases)
1204
+ - api-types.ts (Types namespace for type-only access)
1205
+ - api-enums.ts (Type-safe enum objects from OpenAPI spec)
1206
+ - api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
988
1207
  `);
989
1208
  }
1209
+ // ============================================================================
1210
+ // New helper functions for operation-named API
1211
+ // ============================================================================
1212
+ /**
1213
+ * Parses an already-loaded OpenAPISpec into a map of operationId → OperationInfo.
1214
+ * @param openApiSpec The parsed OpenAPI spec object
1215
+ * @param excludePrefix Operations with this prefix are excluded
1216
+ * @returns Map of operation ID to { path, method }
1217
+ */
1218
+ function buildOperationMap(openApiSpec, excludePrefix) {
1219
+ const map = {};
1220
+ for (const [pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
1221
+ for (const [method, rawOp] of Object.entries(pathItem)) {
1222
+ if (!HTTP_METHODS.includes(method))
1223
+ continue;
1224
+ const op = rawOp;
1225
+ if (!op.operationId)
1226
+ continue;
1227
+ if (excludePrefix && op.operationId.startsWith(excludePrefix))
1228
+ continue;
1229
+ // Extract path and query parameters
1230
+ const pathParams = [];
1231
+ const queryParams = [];
1232
+ if (op.parameters) {
1233
+ for (const param of op.parameters) {
1234
+ const type = param.schema?.type || 'string';
1235
+ if (param.in === 'path') {
1236
+ pathParams.push({ name: param.name, type });
1237
+ }
1238
+ else if (param.in === 'query') {
1239
+ queryParams.push({ name: param.name, type });
1240
+ }
1241
+ }
1242
+ }
1243
+ // Extract request body schema
1244
+ let requestBodySchema;
1245
+ const reqBodyRef = op.requestBody?.content?.['application/json']?.schema?.$ref;
1246
+ if (reqBodyRef) {
1247
+ requestBodySchema = reqBodyRef.split('/').pop();
1248
+ }
1249
+ // Extract response schema (from 200/201 responses)
1250
+ let responseSchema;
1251
+ if (op.responses) {
1252
+ for (const statusCode of ['200', '201']) {
1253
+ const resRef = op.responses[statusCode]?.content?.['application/json']?.schema?.$ref;
1254
+ if (resRef) {
1255
+ responseSchema = resRef.split('/').pop();
1256
+ break;
1257
+ }
1258
+ }
1259
+ }
1260
+ map[op.operationId] = {
1261
+ path: pathUrl,
1262
+ method: method.toUpperCase(),
1263
+ summary: op.summary,
1264
+ description: op.description,
1265
+ pathParams: pathParams.length > 0 ? pathParams : undefined,
1266
+ queryParams: queryParams.length > 0 ? queryParams : undefined,
1267
+ requestBodySchema,
1268
+ responseSchema,
1269
+ };
1270
+ }
1271
+ }
1272
+ return map;
1273
+ }
1274
+ /**
1275
+ * Converts an OpenAPI enum array to `{ MemberName: 'value' }`.
1276
+ * @param values Enum values (may include null)
1277
+ * @returns Object with PascalCase keys and string literal values
1278
+ */
1279
+ function enumArrayToObject(values) {
1280
+ const obj = {};
1281
+ for (const v of values) {
1282
+ if (v === null)
1283
+ continue;
1284
+ obj[toEnumMemberName(v)] = String(v);
1285
+ }
1286
+ return obj;
1287
+ }
1288
+ /**
1289
+ * For each operation, extract enum fields from:
1290
+ * 1. Request body object properties (direct `enum` or `$ref` to an enum schema)
1291
+ * 2. Query and path parameters with `enum`
1292
+ * @param openApiSpec The parsed OpenAPI spec
1293
+ * @param operationMap Map from buildOperationMap
1294
+ * @returns operationId → { fieldName → { MemberName: 'value' } }
1295
+ */
1296
+ function buildOperationEnums(openApiSpec, operationMap) {
1297
+ // Schema-level enum lookup: schemaName → { MemberName: value }
1298
+ const schemaEnumLookup = {};
1299
+ if (openApiSpec.components?.schemas) {
1300
+ for (const [schemaName, schema] of Object.entries(openApiSpec.components.schemas)) {
1301
+ if (schema.enum) {
1302
+ schemaEnumLookup[schemaName] = enumArrayToObject(schema.enum);
1303
+ }
1304
+ }
1305
+ }
1306
+ function resolveEnums(schema) {
1307
+ if (schema.enum)
1308
+ return enumArrayToObject(schema.enum);
1309
+ if (typeof schema.$ref === 'string') {
1310
+ const name = schema.$ref.split('/').pop();
1311
+ return schemaEnumLookup[name] ?? null;
1312
+ }
1313
+ return null;
1314
+ }
1315
+ const result = {};
1316
+ for (const [_pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
1317
+ for (const [method, rawOp] of Object.entries(pathItem)) {
1318
+ if (!HTTP_METHODS.includes(method))
1319
+ continue;
1320
+ const op = rawOp;
1321
+ if (!op.operationId || !(op.operationId in operationMap))
1322
+ continue;
1323
+ const fields = {};
1324
+ // Request body properties
1325
+ const bodyProps = op.requestBody?.content?.['application/json']?.schema?.properties;
1326
+ if (bodyProps) {
1327
+ for (const [fieldName, fieldSchema] of Object.entries(bodyProps)) {
1328
+ const resolved = resolveEnums(fieldSchema);
1329
+ if (resolved)
1330
+ fields[fieldName] = resolved;
1331
+ }
1332
+ }
1333
+ // Query + path parameters
1334
+ for (const param of op.parameters ?? []) {
1335
+ if (param.schema) {
1336
+ const resolved = resolveEnums(param.schema);
1337
+ if (resolved)
1338
+ fields[param.name] = resolved;
1339
+ }
1340
+ }
1341
+ result[op.operationId] = fields;
1342
+ }
1343
+ }
1344
+ return result;
1345
+ }
1346
+ // ============================================================================
1347
+ // New generators: api-operations.ts
1348
+ // ============================================================================
1349
+ /**
1350
+ * Generates the content for `api-operations.ts` file.
1351
+ * @param operationMap The operation info map
1352
+ * @param opEnums Per-operation enum fields
1353
+ * @param schemaEnumNames Names from api-enums.ts to re-export
1354
+ * @returns Generated TypeScript file content
1355
+ */
1356
+ function generateApiOperationsContent(operationMap, opEnums, schemaEnumNames) {
1357
+ const ids = Object.keys(operationMap).sort();
1358
+ const _queryIds = ids.filter((id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method));
1359
+ const _mutationIds = ids.filter((id) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(operationMap[id].method));
1360
+ // Per-operation enum consts
1361
+ const enumConsts = ids
1362
+ .map((id) => {
1363
+ const fields = opEnums[id] ?? {};
1364
+ const body = Object.entries(fields)
1365
+ .map(([field, vals]) => {
1366
+ const members = Object.entries(vals)
1367
+ .map(([k, v]) => ` ${k}: ${JSON.stringify(v)} as const,`)
1368
+ .join('\n');
1369
+ return ` ${field}: {\n${members}\n } as const,`;
1370
+ })
1371
+ .join('\n');
1372
+ return `export const ${id}_enums = {\n${body}\n} as const`;
1373
+ })
1374
+ .join('\n\n');
1375
+ // Operations map
1376
+ const opEntries = ids
1377
+ .map((id) => ` ${id}: { path: '${operationMap[id].path}', method: HttpMethod.${operationMap[id].method} },`)
1378
+ .join('\n');
1379
+ // Type helpers — now use openapi-typescript `operations` directly (not OpenApiOperations)
1380
+ const typeHelpers = `
1381
+ type AllOps = keyof operations
1382
+
1383
+ /** Response data type for an operation (all fields required). */
1384
+ export type ApiResponse<K extends AllOps> = _ApiResponse<operations, K>
1385
+ /** Response data type - only \`readonly\` fields required. */
1386
+ export type ApiResponseSafe<K extends AllOps> = _ApiResponseSafe<operations, K>
1387
+ /** Request body type. */
1388
+ export type ApiRequest<K extends AllOps> = _ApiRequest<operations, K>
1389
+ /** Path parameters type. */
1390
+ export type ApiPathParams<K extends AllOps> = _ApiPathParams<operations, K>
1391
+ /** Path parameters input type (allows undefined values for reactive resolution). */
1392
+ export type ApiPathParamsInput<K extends AllOps> = _ApiPathParamsInput<operations, K>
1393
+ /** Query parameters type. */
1394
+ export type ApiQueryParams<K extends AllOps> = _ApiQueryParams<operations, K>`;
1395
+ // Re-exports
1396
+ // Use type-only wildcard export to avoid duplicate identifier errors
1397
+ const reExports = schemaEnumNames.length > 0
1398
+ ? schemaEnumNames.map((n) => `export { ${n} } from './api-enums'`).join('\n') +
1399
+ "\nexport type * from './api-enums'"
1400
+ : '// No schema-level enums to re-export';
1401
+ return `// Auto-generated from OpenAPI specification - do not edit manually
1402
+
1403
+ import type { operations } from './openapi-types.js'
1404
+ import { HttpMethod } from '@qualisero/openapi-endpoint'
1405
+ import type {
1406
+ ApiResponse as _ApiResponse,
1407
+ ApiResponseSafe as _ApiResponseSafe,
1408
+ ApiRequest as _ApiRequest,
1409
+ ApiPathParams as _ApiPathParams,
1410
+ ApiPathParamsInput as _ApiPathParamsInput,
1411
+ ApiQueryParams as _ApiQueryParams,
1412
+ } from '@qualisero/openapi-endpoint'
1413
+
1414
+ export type { operations }
1415
+
1416
+ ${reExports}
1417
+
1418
+ // ============================================================================
1419
+ // Per-operation enum values
1420
+ // ============================================================================
1421
+
1422
+ ${enumConsts}
1423
+
1424
+ // ============================================================================
1425
+ // Operations map (kept for inspection / backward compatibility)
1426
+ // ============================================================================
1427
+
1428
+ const operationsBase = {
1429
+ ${opEntries}
1430
+ } as const
1431
+
1432
+ export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
1433
+ export type OpenApiOperations = typeof openApiOperations
1434
+
1435
+ // ============================================================================
1436
+ // Convenience type aliases
1437
+ // ============================================================================
1438
+ ${typeHelpers}
1439
+ `;
1440
+ }
1441
+ /**
1442
+ * Async wrapper for generateApiOperationsContent.
1443
+ */
1444
+ async function generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames) {
1445
+ console.log('🔨 Generating api-operations.ts...');
1446
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1447
+ const opEnums = buildOperationEnums(openApiSpec, operationMap);
1448
+ const content = generateApiOperationsContent(operationMap, opEnums, schemaEnumNames);
1449
+ fs.writeFileSync(path.join(outputDir, 'api-operations.ts'), content);
1450
+ console.log(`✅ Generated api-operations.ts (${Object.keys(operationMap).length} operations)`);
1451
+ }
1452
+ // ============================================================================
1453
+ // New generators: api-types.ts
1454
+ // ============================================================================
1455
+ /**
1456
+ * Generates the content for `api-types.ts` file.
1457
+ * @param operationMap The operation info map
1458
+ * @param opEnums Per-operation enum fields
1459
+ * @returns Generated TypeScript file content
1460
+ */
1461
+ function generateApiTypesContent(operationMap, opEnums) {
1462
+ const ids = Object.keys(operationMap).sort();
1463
+ const isQuery = (id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method);
1464
+ const namespaces = ids
1465
+ .map((id) => {
1466
+ const query = isQuery(id);
1467
+ const fields = opEnums[id] ?? {};
1468
+ const enumTypes = Object.entries(fields)
1469
+ .map(([fieldName, vals]) => {
1470
+ const typeName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
1471
+ const union = Object.values(vals)
1472
+ .map((v) => `'${v}'`)
1473
+ .join(' | ');
1474
+ return ` /** \`${union}\` */\n export type ${typeName} = ${union}`;
1475
+ })
1476
+ .join('\n');
1477
+ const commonLines = [
1478
+ ` /** Full response type - all fields required. */`,
1479
+ ` export type Response = _ApiResponse<OpenApiOperations, '${id}'>`,
1480
+ ` /** Response type - only \`readonly\` fields required. */`,
1481
+ ` export type SafeResponse = _ApiResponseSafe<OpenApiOperations, '${id}'>`,
1482
+ ];
1483
+ if (!query) {
1484
+ commonLines.push(` /** Request body type. */`, ` export type Request = _ApiRequest<OpenApiOperations, '${id}'>`);
1485
+ }
1486
+ commonLines.push(` /** Path parameters. */`, ` export type PathParams = _ApiPathParams<OpenApiOperations, '${id}'>`, ` /** Query parameters. */`, ` export type QueryParams = _ApiQueryParams<OpenApiOperations, '${id}'>`);
1487
+ const enumNs = enumTypes ? ` export namespace Enums {\n${enumTypes}\n }` : ` export namespace Enums {}`;
1488
+ return ` export namespace ${id} {\n${commonLines.join('\n')}\n${enumNs}\n }`;
1489
+ })
1490
+ .join('\n\n');
1491
+ return `/* eslint-disable */
1492
+ // Auto-generated from OpenAPI specification — do not edit manually
1493
+
1494
+ import type {
1495
+ ApiResponse as _ApiResponse,
1496
+ ApiResponseSafe as _ApiResponseSafe,
1497
+ ApiRequest as _ApiRequest,
1498
+ ApiPathParams as _ApiPathParams,
1499
+ ApiQueryParams as _ApiQueryParams,
1500
+ } from '@qualisero/openapi-endpoint'
1501
+ import type { operations as OpenApiOperations } from './openapi-types.js'
1502
+
1503
+ /**
1504
+ * Type-only namespace for all API operations.
1505
+ *
1506
+ * @example
1507
+ * \`\`\`ts
1508
+ * import type { Types } from './generated/api-types'
1509
+ *
1510
+ * type Pet = Types.getPet.Response
1511
+ * type NewPet = Types.createPet.Request
1512
+ * type PetStatus = Types.createPet.Enums.Status // 'available' | 'pending' | 'adopted'
1513
+ * type Params = Types.getPet.PathParams // { petId: string }
1514
+ * \`\`\`
1515
+ */
1516
+ export namespace Types {
1517
+ ${namespaces}
1518
+ }
1519
+ `;
1520
+ }
1521
+ /**
1522
+ * Async wrapper for generateApiTypesContent.
1523
+ */
1524
+ async function generateApiTypesFile(openApiSpec, outputDir, excludePrefix) {
1525
+ console.log('🔨 Generating api-types.ts...');
1526
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1527
+ const opEnums = buildOperationEnums(openApiSpec, operationMap);
1528
+ const content = generateApiTypesContent(operationMap, opEnums);
1529
+ fs.writeFileSync(path.join(outputDir, 'api-types.ts'), content);
1530
+ console.log(`✅ Generated api-types.ts`);
1531
+ }
990
1532
  async function main() {
991
1533
  const args = process.argv.slice(2);
992
1534
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
@@ -1030,18 +1572,22 @@ async function main() {
1030
1572
  else {
1031
1573
  console.log(`✅ Including all operations (no exclusion filter)`);
1032
1574
  }
1033
- // Fetch OpenAPI spec content
1575
+ // Fetch and parse OpenAPI spec once
1034
1576
  let openapiContent = await fetchOpenAPISpec(openapiInput);
1035
- // Parse spec and add missing operationIds
1036
1577
  const openApiSpec = JSON.parse(openapiContent);
1578
+ // Add missing operationIds
1037
1579
  addMissingOperationIds(openApiSpec);
1038
1580
  openapiContent = JSON.stringify(openApiSpec, null, 2);
1581
+ // Collect schema enum names for re-export
1582
+ const schemaEnumNames = extractEnumsFromSpec(openApiSpec).map((e) => e.name);
1039
1583
  // Generate all files
1040
1584
  await Promise.all([
1041
1585
  generateTypes(openapiContent, outputDir),
1042
- generateApiOperations(openapiContent, outputDir, excludePrefix),
1043
1586
  generateApiEnums(openapiContent, outputDir, excludePrefix),
1044
1587
  generateApiSchemas(openapiContent, outputDir, excludePrefix),
1588
+ generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames),
1589
+ generateApiTypesFile(openApiSpec, outputDir, excludePrefix),
1590
+ generateApiClientFile(openApiSpec, outputDir, excludePrefix),
1045
1591
  ]);
1046
1592
  console.log('🎉 Code generation completed successfully!');
1047
1593
  }