Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"cSpell.words": ["fets"]
}
4 changes: 2 additions & 2 deletions e2e/shared-scripts/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { OutputValue, Stack } from '@pulumi/pulumi/automation';

export type DeploymentConfiguration<TProgramOutput = any> = {
name: string;
prerequisites?: (stack: Stack) => Promise<void>;
config?: (stack: Stack) => Promise<void>;
prerequisites?: ((stack: Stack) => Promise<void>) | undefined;
config?: ((stack: Stack) => Promise<void>) | undefined;
program: () => Promise<{
[K in keyof TProgramOutput]: Output<TProgramOutput[K]> | TProgramOutput[K];
}>;
Expand Down
4 changes: 2 additions & 2 deletions e2e/shared-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createRouter, Response, useErrorHandling } from 'fets';
import { z } from 'zod';

export function createTestServerAdapter<TServerContext = {}>(base?: string) {
return createRouter<TServerContext, {}>({
export function createTestServerAdapter<TServerContext = {}>(base?: string | undefined) {
return createRouter<TServerContext, { schemas: {} }>({
base,
plugins: [useErrorHandling()],
})
Expand Down
6 changes: 5 additions & 1 deletion e2e/vercel/scripts/createVercelDeployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ class VercelProvider implements pulumi.dynamic.ResourceProvider {
export class VercelDeployment extends pulumi.dynamic.Resource {
public readonly url!: pulumi.Output<string>;

constructor(name: string, props: VercelDeploymentInputs, opts?: pulumi.CustomResourceOptions) {
constructor(
name: string,
props: VercelDeploymentInputs,
opts?: pulumi.CustomResourceOptions | undefined,
) {
super(
new VercelProvider(),
name,
Expand Down
4 changes: 2 additions & 2 deletions packages/fets/src/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const defaultSerializer: JSONSerializer = obj => JSON.stringify(obj);
export interface LazySerializedResponse {
[LAZY_SERIALIZED_RESPONSE]: true;
resolveWithSerializer(serializer: JSONSerializer): void;
init?: ResponseInit;
init?: ResponseInit | undefined;
actualResponse: Response;
jsonObj: any;
json: () => Promise<any>;
Expand All @@ -27,7 +27,7 @@ function isHeadersLike(headers: any): headers is Headers {

const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';

function getHeadersFromHeadersInit(init?: HeadersInit): Headers {
function getHeadersFromHeadersInit(init?: HeadersInit | undefined): Headers {
let headers: Headers;
if (isHeadersLike(init)) {
headers = init;
Expand Down
14 changes: 7 additions & 7 deletions packages/fets/src/client/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type OASOAuthPathRequestParamsWithHeader = {
/**
* Your service can support different scopes for the client credentials grant. In practice, not many services actually support this.
*/
scope?: string | string[];
scope?: string | string[] | undefined;
};
headers: {
/**
Expand Down Expand Up @@ -71,7 +71,7 @@ export type OASOAuthPath<TOAS> = TOAS extends {
requestParams:
| OASOAuthPathRequestParamsWithHeader
| OASOAuthPathRequestParamsWithoutHeader,
requestInit?: RequestInit,
requestInit?: RequestInit | undefined,
): Promise<
TypedResponseWithJSONStatusMap<{
200: OAuthPathSuccessResponse;
Expand All @@ -98,19 +98,19 @@ export type OAuthPathSuccessResponse = {
/**
* If the access token expires, the server should reply with the duration of time the access token is granted for.
*/
expires_in?: number;
expires_in?: number | undefined;
/**
* If the access token will expire,
* then it is useful to return a refresh token which applications can use to obtain another access token.
* However, tokens issued with the implicit grant cannot be issued a refresh token.
*/
refresh_token?: string;
refresh_token?: string | undefined;
/**
* If the scope the user granted is identical to the scope the app requested, this parameter is optional.
* If the granted scope is different from the requested scope,
* such as if the user modified the scope, then this parameter is required.
*/
scope?: string;
scope?: string | undefined;
};

/**
Expand All @@ -128,11 +128,11 @@ export interface OAuthPathFailedResponse {
/**
* The error_description parameter can only include ASCII characters, and should be a sentence or two at most describing the circumstance of the error.
*/
error_description?: string;
error_description?: string | undefined;
/**
* The error_uri is a great place to link to your API documentation for information about how to correct the specific error that was encountered.
*/
error_uri?: string;
error_uri?: string | undefined;
}

export enum OAuthPathErrorType {
Expand Down
77 changes: 43 additions & 34 deletions packages/fets/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export type OASParamObj<
schema: JSONSchema;
}
? FromSchema<TParameter['schema']>
: TParameter extends { type: JSONSchema7TypeName; enum?: any[] }
: TParameter extends { type: JSONSchema7TypeName; enum?: any[] | undefined }
? FromSchema<{
type: TParameter['type'];
enum: TParameter['enum'];
Expand All @@ -146,7 +146,7 @@ interface OASParamToRequestParam<TParameters extends { in: string; required?: bo
extends Fn {
return: this['arg0'] extends { name: string; in: infer TParamType }
? // If there is any required parameter for this parameter type, make that parameter type required
TParameters extends [{ in: TParamType; required?: true }]
TParameters extends [{ in: TParamType; required?: true | undefined }]
? {
[TKey in TParamType extends keyof OASParamPropMap
? OASParamPropMap[TParamType]
Expand All @@ -155,7 +155,7 @@ interface OASParamToRequestParam<TParameters extends { in: string; required?: bo
: {
[TKey in TParamType extends keyof OASParamPropMap
? OASParamPropMap[TParamType]
: never]?: OASParamObj<this['arg0']>;
: never]?: OASParamObj<this['arg0']> | undefined;
}
: {};
}
Expand Down Expand Up @@ -188,11 +188,11 @@ export type OASClient<TOAS extends OpenAPIDocument> = {
}
? (
requestParams: Simplify<OASRequestParams<TOAS, TPath, TMethod>>,
init?: RequestInit,
init?: RequestInit | undefined,
) => Promise<OASResponse<TOAS, TPath, TMethod>>
: (
requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod>>,
init?: RequestInit,
requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod>> | undefined,
init?: RequestInit | undefined,
) => Promise<OASResponse<TOAS, TPath, TMethod>>;
};
} & OASOAuthPath<TOAS>;
Expand Down Expand Up @@ -273,9 +273,14 @@ export type OASRequestParams<
*
* The value of `json` will be stringified and sent as the request body with `Content-Type: application/json`.
*/
json?: FromSchema<
OASMethodMap<TOAS, TPath>[TMethod]['requestBody']['content']['application/json']['schema']
>;
json?:
| FromSchema<
OASMethodMap<
TOAS,
TPath
>[TMethod]['requestBody']['content']['application/json']['schema']
>
| undefined;
}
: OASMethodMap<TOAS, TPath>[TMethod] extends {
requestBody: { content: { 'multipart/form-data': { schema: JSONSchema } } };
Expand All @@ -300,12 +305,14 @@ export type OASRequestParams<
*
* The value of `formData` will be sent as the request body with `Content-Type: multipart/form-data`.
*/
formData?: FromSchema<
OASMethodMap<
TOAS,
TPath
>[TMethod]['requestBody']['content']['multipart/form-data']['schema']
>;
formData?:
| FromSchema<
OASMethodMap<
TOAS,
TPath
>[TMethod]['requestBody']['content']['multipart/form-data']['schema']
>
| undefined;
}
: OASMethodMap<TOAS, TPath>[TMethod] extends {
requestBody: { content: { 'application/x-www-form-urlencoded': { schema: JSONSchema } } };
Expand All @@ -330,12 +337,14 @@ export type OASRequestParams<
*
* The value of `formUrlEncoded` will be sent as the request body with `Content-Type: application/x-www-form-urlencoded`.
*/
formUrlEncoded?: FromSchema<
OASMethodMap<
TOAS,
TPath
>[TMethod]['requestBody']['content']['application/x-www-form-urlencoded']['schema']
>;
formUrlEncoded?:
| FromSchema<
OASMethodMap<
TOAS,
TPath
>[TMethod]['requestBody']['content']['application/x-www-form-urlencoded']['schema']
>
| undefined;
}
: {}) &
(OASMethodMap<TOAS, TPath>[TMethod] extends { parameters: { name: string; in: string }[] }
Expand Down Expand Up @@ -399,19 +408,19 @@ export interface ClientOptions {
/**
* The base URL of the API
*/
endpoint?: string;
endpoint?: string | undefined;
/**
* WHATWG compatible fetch implementation
*
* @see https://the-guild.dev/openapi/fets/client/client-configuration#customizing-the-fetch-function
*/
fetchFn?: typeof fetch;
fetchFn?: typeof fetch | undefined;
/**
* Plugins to extend the client functionality
*
* @see https://the-guild.dev/openapi/fets/client/plugins
*/
plugins?: ClientPlugin[];
plugins?: ClientPlugin[] | undefined;
}

export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit<
Expand Down Expand Up @@ -443,24 +452,24 @@ export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit
endpoint: `${TProtocol}://${THost}${TBasePath}`;
}
: {
endpoint?: string;
endpoint?: string | undefined;
});

export interface ClientRequestParams {
json?: any;
formData?: FormData;
formUrlEncoded?: Record<string, string | string[]>;
params?: Record<string, string>;
query?: Record<string, string | string[]>;
headers?: Record<string, string>;
formData?: FormData | undefined;
formUrlEncoded?: Record<string, string | string[]> | undefined;
params?: Record<string, string> | undefined;
query?: Record<string, string | string[]> | undefined;
headers?: Record<string, string> | undefined;
}

export type ClientMethod = (requestParams?: ClientRequestParams) => Promise<Response>;
export type ClientMethod = (requestParams?: ClientRequestParams | undefined) => Promise<Response>;

export interface ClientPlugin {
onRequestInit?: OnRequestInitHook;
onFetch?: OnFetchHook;
onResponse?: OnResponseHook;
onRequestInit?: OnRequestInitHook | undefined;
onFetch?: OnFetchHook | undefined;
onResponse?: OnResponseHook | undefined;
}

export type OnRequestInitHook = (payload: ClientOnRequestInitPayload) => Promise<void> | void;
Expand Down
33 changes: 28 additions & 5 deletions packages/fets/src/plugins/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import { getHeadersObj } from './utils.js';

type ValidateRequestFn = (request: RouterRequest) => PromiseOrValue<ErrorObject[]>;

const defaultComponents: RouterComponentsBase = { schemas: {} };

export function useAjv({
components = {},
components = defaultComponents,
}: {
components?: RouterComponentsBase;
components?: RouterComponentsBase | undefined;
} = {}): RouterPlugin<any> {
const ajv = new Ajv({
strict: false,
Expand Down Expand Up @@ -73,10 +75,15 @@ export function useAjv({
onRoute({ path, schemas, handlers }) {
const validationMiddlewares = new Map<string, ValidateRequestFn>();
if (schemas?.request?.headers && !isZodSchema(schemas.request.headers)) {
const validateFn = ajv.compile({
...schemas.request.headers,
const target = {
components,
});
...schemas.request.headers,
};

// TODO: possible bug?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const validateFn = ajv.compile(target);
validationMiddlewares.set('headers', request => {
const headersObj = getHeadersObj(request.headers);
const isValid = validateFn(headersObj);
Expand All @@ -87,10 +94,14 @@ export function useAjv({
});
}
if (schemas?.request?.params && !isZodSchema(schemas.request.params)) {
// TODO: possible bug?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const validateFn = ajv.compile({
...schemas.request.params,
components,
});

validationMiddlewares.set('params', request => {
const isValid = validateFn(request.params);
if (!isValid) {
Expand All @@ -100,10 +111,14 @@ export function useAjv({
});
}
if (schemas?.request?.query && !isZodSchema(schemas.request.query)) {
// TODO: possible bug?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const validateFn = ajv.compile({
...schemas.request.query,
components,
});

validationMiddlewares.set('query', request => {
const isValid = validateFn(request.query);
if (!isValid) {
Expand All @@ -113,10 +128,14 @@ export function useAjv({
});
}
if (schemas?.request?.json && !isZodSchema(schemas.request.json)) {
// TODO: possible bug?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const validateFn = ajv.compile({
...schemas.request.json,
components,
});

validationMiddlewares.set('json', async request => {
const contentType = request.headers.get('content-type');
if (contentType?.includes('json')) {
Expand All @@ -134,10 +153,14 @@ export function useAjv({
});
}
if (schemas?.request?.formData && !isZodSchema(schemas.request.formData)) {
// TODO: possible bug?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const validateFn = ajv.compile({
...schemas.request.formData,
components,
});

validationMiddlewares.set('formData', async request => {
const contentType = request.headers.get('content-type');
if (
Expand Down
Loading