Skip to content

Commit ecd1ce7

Browse files
committed
test: add "failing" test
1 parent dad912c commit ecd1ce7

File tree

1 file changed

+210
-63
lines changed

1 file changed

+210
-63
lines changed

packages/typed-openapi/tests/generator.test.ts

Lines changed: 210 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -523,69 +523,6 @@ describe("generator", () => {
523523
},
524524
},
525525
"paths": {
526-
// "/authentication/refresh": {
527-
// "post": {
528-
// "summary": "Refresh authentication tokens",
529-
// "tags": [
530-
// "Authentication"
531-
// ],
532-
// "requestBody": {
533-
// "content": {
534-
// "application/json": {
535-
// "schema": {
536-
// "type": "object",
537-
// "properties": {
538-
// "accessToken": {
539-
// "type": "string",
540-
// "pattern": "^\\w+ [A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*$"
541-
// },
542-
// "refreshToken": {
543-
// "type": "string",
544-
// "format": "uuid"
545-
// }
546-
// },
547-
// "required": [
548-
// "accessToken",
549-
// "refreshToken"
550-
// ]
551-
// }
552-
// }
553-
// }
554-
// },
555-
// "responses": {
556-
// "200": {
557-
// "description": "Refresh the access and refresh tokens",
558-
// "content": {
559-
// "application/json": {
560-
// "schema": {
561-
// "$ref": "#/components/schemas/SerializedUserSession"
562-
// }
563-
// }
564-
// }
565-
// },
566-
// "401": {
567-
// "description": "Unauthorized",
568-
// "content": {
569-
// "application/json": {
570-
// "schema": {
571-
// "type": "string"
572-
// }
573-
// }
574-
// }
575-
// },
576-
// "500": {
577-
// "description": "Internal server error",
578-
// "content": {
579-
// "application/json": {
580-
// "schema": {
581-
// "type": "string"
582-
// }
583-
// }
584-
// }
585-
// }
586-
// }
587-
// }
588-
// },
589526
"/authorization/organizations/:organizationId/members/search": {
590527
"get": {
591528
"summary": "Search for members in an organization",
@@ -878,4 +815,214 @@ describe("generator", () => {
878815
"
879816
`);
880817
});
818+
819+
test.only("optional query param", async ({ expect }) => {
820+
expect(await prettify(generateFile(mapOpenApiEndpoints({
821+
"openapi": "3.0.0",
822+
"info": {
823+
"version": "1.0.0",
824+
"title": "Demo API"
825+
},
826+
"paths": {
827+
"/demo": {
828+
"get": {
829+
"parameters": [
830+
{
831+
"schema": {
832+
"type": "string",
833+
"format": "uuid"
834+
},
835+
"required": true,
836+
"name": "organizationId",
837+
"in": "query"
838+
},
839+
{
840+
"schema": {
841+
"type": "string"
842+
},
843+
"required": false,
844+
"name": "searchQuery",
845+
"in": "query"
846+
},
847+
{
848+
"schema": {
849+
"type": "string",
850+
"format": "uuid"
851+
},
852+
"required": false,
853+
"name": "optionalInPath1",
854+
"in": "path"
855+
},
856+
{
857+
"schema": {
858+
"type": "string"
859+
},
860+
"required": false,
861+
"name": "optionalInPath2",
862+
"in": "path"
863+
},
864+
],
865+
"responses": {
866+
"200": {
867+
"content": {
868+
"application/json": {
869+
"schema": {
870+
"type": "string",
871+
}
872+
}
873+
}
874+
}
875+
}
876+
}
877+
},
878+
}
879+
})))).toMatchInlineSnapshot(`
880+
"export namespace Schemas {
881+
// <Schemas>
882+
// </Schemas>
883+
}
884+
885+
export namespace Endpoints {
886+
// <Endpoints>
887+
888+
export type get__demo = {
889+
method: "GET";
890+
path: "/demo";
891+
requestFormat: "json";
892+
parameters: {
893+
query: { organizationId: string; searchQuery: string | undefined };
894+
path: Partial<{ optionalInPath1: string; optionalInPath2: string }>;
895+
};
896+
response: string;
897+
};
898+
899+
// </Endpoints>
900+
}
901+
902+
// <EndpointByMethod>
903+
export type EndpointByMethod = {
904+
get: {
905+
"/demo": Endpoints.get__demo;
906+
};
907+
};
908+
909+
// </EndpointByMethod>
910+
911+
// <EndpointByMethod.Shorthands>
912+
export type GetEndpoints = EndpointByMethod["get"];
913+
// </EndpointByMethod.Shorthands>
914+
915+
// <ApiClientTypes>
916+
export type EndpointParameters = {
917+
body?: unknown;
918+
query?: Record<string, unknown>;
919+
header?: Record<string, unknown>;
920+
path?: Record<string, unknown>;
921+
};
922+
923+
export type MutationMethod = "post" | "put" | "patch" | "delete";
924+
export type Method = "get" | "head" | "options" | MutationMethod;
925+
926+
type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
927+
928+
export type DefaultEndpoint = {
929+
parameters?: EndpointParameters | undefined;
930+
response: unknown;
931+
};
932+
933+
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
934+
operationId: string;
935+
method: Method;
936+
path: string;
937+
requestFormat: RequestFormat;
938+
parameters?: TConfig["parameters"];
939+
meta: {
940+
alias: string;
941+
hasParameters: boolean;
942+
areParametersRequired: boolean;
943+
};
944+
response: TConfig["response"];
945+
};
946+
947+
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
948+
949+
type RequiredKeys<T> = {
950+
[P in keyof T]-?: undefined extends T[P] ? never : P;
951+
}[keyof T];
952+
953+
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
954+
955+
// </ApiClientTypes>
956+
957+
// <ApiClient>
958+
export class ApiClient {
959+
baseUrl: string = "";
960+
961+
constructor(public fetcher: Fetcher) {}
962+
963+
setBaseUrl(baseUrl: string) {
964+
this.baseUrl = baseUrl;
965+
return this;
966+
}
967+
968+
parseResponse = async <T,>(response: Response): Promise<T> => {
969+
const contentType = response.headers.get("content-type");
970+
if (contentType?.includes("application/json")) {
971+
return response.json();
972+
}
973+
return response.text() as unknown as T;
974+
};
975+
976+
// <ApiClient.get>
977+
get<Path extends keyof GetEndpoints, TEndpoint extends GetEndpoints[Path]>(
978+
path: Path,
979+
...params: MaybeOptionalArg<TEndpoint["parameters"]>
980+
): Promise<TEndpoint["response"]> {
981+
return this.fetcher("get", this.baseUrl + path, params[0]).then((response) =>
982+
this.parseResponse(response),
983+
) as Promise<TEndpoint["response"]>;
984+
}
985+
// </ApiClient.get>
986+
987+
// <ApiClient.request>
988+
/**
989+
* Generic request method with full type-safety for any endpoint
990+
*/
991+
request<
992+
TMethod extends keyof EndpointByMethod,
993+
TPath extends keyof EndpointByMethod[TMethod],
994+
TEndpoint extends EndpointByMethod[TMethod][TPath],
995+
>(
996+
method: TMethod,
997+
path: TPath,
998+
...params: MaybeOptionalArg<TEndpoint extends { parameters: infer Params } ? Params : never>
999+
): Promise<
1000+
Omit<Response, "json"> & {
1001+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
1002+
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
1003+
}
1004+
> {
1005+
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
1006+
}
1007+
// </ApiClient.request>
1008+
}
1009+
1010+
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
1011+
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
1012+
}
1013+
1014+
/**
1015+
Example usage:
1016+
const api = createApiClient((method, url, params) =>
1017+
fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
1018+
);
1019+
api.get("/users").then((users) => console.log(users));
1020+
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
1021+
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
1022+
*/
1023+
1024+
// </ApiClient
1025+
"
1026+
`);
1027+
});
8811028
});

0 commit comments

Comments
 (0)