Bring back content negotiation exports as well as define a new response type system.refactor/new-arch
@@ -50,3 +50,8 @@ class BackendInstance<App extends BaseApp> implements Backend<App> { | |||||
export const backend = <App extends BaseApp>(params: BackendParams<App>): Backend<App> => { | export const backend = <App extends BaseApp>(params: BackendParams<App>): Backend<App> => { | ||||
return new BackendInstance(params); | return new BackendInstance(params); | ||||
}; | }; | ||||
export class PlainResponse { | |||||
constructor() { | |||||
} | |||||
} |
@@ -5,7 +5,7 @@ export interface ClientParams<App extends BaseApp> { | |||||
fetch?: typeof fetch; | fetch?: typeof fetch; | ||||
} | } | ||||
export interface Client<App extends BaseApp> { | |||||
export interface Client<App extends BaseApp = BaseApp> { | |||||
app: App; | app: App; | ||||
connect(params: ServiceParams): this; | connect(params: ServiceParams): this; | ||||
at<TheEndpoint extends Endpoint = Endpoint>(endpoint: TheEndpoint, params?: Record<GetEndpointParams<TheEndpoint>, unknown>): this; | at<TheEndpoint extends Endpoint = Endpoint>(endpoint: TheEndpoint, params?: Record<GetEndpointParams<TheEndpoint>, unknown>): this; | ||||
@@ -1,5 +1,5 @@ | |||||
import {Endpoint, EndpointOperations} from './endpoint'; | import {Endpoint, EndpointOperations} from './endpoint'; | ||||
import {BaseOperationParams, Operation} from './operation'; | |||||
import {Operation} from './operation'; | |||||
export interface BaseAppState { | export interface BaseAppState { | ||||
endpoints: unknown; | endpoints: unknown; | ||||
@@ -21,31 +21,11 @@ export interface App<AppName extends string = string, AppState extends BaseAppSt | |||||
name: AppName; | name: AppName; | ||||
operations: Set<Operation>; | operations: Set<Operation>; | ||||
endpoints: Set<Endpoint>; | endpoints: Set<Endpoint>; | ||||
operation< | |||||
OperationName extends string, | |||||
OperationParams extends BaseOperationParams<OperationName>, | |||||
NewOperation extends Operation<OperationParams> | |||||
>(newOperation: NewOperation): App< | |||||
operation<NewOperation extends Operation>(newOperation: NewOperation): App< | |||||
AppName, | AppName, | ||||
{ | { | ||||
endpoints: AppState['endpoints'], | endpoints: AppState['endpoints'], | ||||
operations: keyof AppState['operations'] extends never ? { | |||||
[Key in NewOperation['name']]: ( | |||||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||||
) | |||||
} : { | |||||
[Key in NewOperation['name'] | keyof AppState['operations']]: ( | |||||
AppState['operations'] extends Record<Key, any> | |||||
? ( | |||||
AppState['operations'][Key] extends readonly string[] ? AppState['operations'][Key] : ( | |||||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||||
) | |||||
) | |||||
: ( | |||||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||||
) | |||||
); | |||||
} | |||||
operations: AppState['operations'] extends Array<unknown> ? [...AppState['operations'], NewOperation['name']] : [NewOperation['name']] | |||||
} | } | ||||
>; | >; | ||||
endpoint<NewEndpoint extends Endpoint = Endpoint>(newEndpoint: EndpointOperations<NewEndpoint> extends AppOperations<this> ? NewEndpoint : never): App< | endpoint<NewEndpoint extends Endpoint = Endpoint>(newEndpoint: EndpointOperations<NewEndpoint> extends AppOperations<this> ? NewEndpoint : never): App< | ||||
@@ -0,0 +1,11 @@ | |||||
export interface Charset<Name extends string = string> { | |||||
name: Name; | |||||
encode: (str: string) => Buffer; | |||||
decode: (buf: Buffer) => string; | |||||
} | |||||
export const FALLBACK_CHARSET = { | |||||
encode: (str: string) => Buffer.from(str, 'utf-8'), | |||||
decode: (buf: Buffer) => buf.toString('utf-8'), | |||||
name: 'utf-8' as const, | |||||
} satisfies Charset; |
@@ -17,6 +17,7 @@ export const serializeEndpointQueue = (endpointQueue: EndpointQueue) => { | |||||
.map((s) => `/${s}`) | .map((s) => `/${s}`) | ||||
.join(''); | .join(''); | ||||
}) | }) | ||||
.join('') | |||||
}; | }; | ||||
export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: Set<Endpoint>) => { | export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: Set<Endpoint>) => { | ||||
@@ -1,6 +1,9 @@ | |||||
export * from './app'; | export * from './app'; | ||||
export * from './charset'; | |||||
export * from './data-source'; | export * from './data-source'; | ||||
export * from './endpoint'; | export * from './endpoint'; | ||||
export * from './language'; | |||||
export * from './media-type'; | |||||
export * from './operation'; | export * from './operation'; | ||||
export * from './service'; | export * from './service'; | ||||
export * as validation from 'valibot'; | export * as validation from 'valibot'; |
@@ -0,0 +1,342 @@ | |||||
export type MessageBody = string | string[] | (string | string[])[]; | |||||
export const LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS = [ | |||||
'unableToInitializeResourceDataSource', | |||||
'unableToFetchResourceCollection', | |||||
'unableToFetchResource', | |||||
'resourceIdNotGiven', | |||||
'languageNotAcceptable', | |||||
'characterSetNotAcceptable', | |||||
'mediaTypeNotAcceptable', | |||||
'methodNotAllowed', | |||||
'urlNotFound', | |||||
'badRequest', | |||||
'deleteNonExistingResource', | |||||
'unableToCreateResource', | |||||
'unableToBindResourceDataSource', | |||||
'unableToGenerateIdFromResourceDataSource', | |||||
'unableToAssignIdFromResourceDataSource', | |||||
'unableToEmplaceResource', | |||||
'unableToSerializeResponse', | |||||
'unableToEncodeResponse', | |||||
'unableToDeleteResource', | |||||
'unableToDeserializeResource', | |||||
'unableToDecodeResource', | |||||
'unableToDeserializeRequest', | |||||
'patchNonExistingResource', | |||||
'unableToPatchResource', | |||||
'invalidResourcePatch', | |||||
'invalidResourcePatchType', | |||||
'invalidResource', | |||||
'notImplemented', | |||||
'internalServerError', | |||||
'resourceNotFound', | |||||
] as const; | |||||
export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ | |||||
...LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS, | |||||
'ok', | |||||
'resourceCollectionFetched', | |||||
'resourceCollectionQueried', | |||||
'resourceFetched', | |||||
'resourceDeleted', | |||||
'resourcePatched', | |||||
'resourceCreated', | |||||
'resourceReplaced', | |||||
'provideOptions', | |||||
] as const; | |||||
export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number]; | |||||
export interface LanguageStatusMessageMap extends Record<LanguageDefaultStatusMessageKey, string> {} | |||||
export type LanguageDefaultErrorStatusMessageKey = typeof LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS[number]; | |||||
export interface LanguageBodyMap extends Record<LanguageDefaultErrorStatusMessageKey, MessageBody> {} | |||||
export interface Language<Name extends string = string> { | |||||
name: Name, | |||||
statusMessages: LanguageStatusMessageMap, | |||||
bodies: LanguageBodyMap | |||||
} | |||||
export const FALLBACK_LANGUAGE = { | |||||
name: 'en' as const, | |||||
statusMessages: { | |||||
unableToSerializeResponse: 'Unable To Serialize Response', | |||||
unableToEncodeResponse: 'Unable To Encode Response', | |||||
unableToBindResourceDataSource: 'Unable To Bind $RESOURCE Data Source', | |||||
unableToInitializeResourceDataSource: 'Unable To Initialize $RESOURCE Data Source', | |||||
unableToFetchResourceCollection: 'Unable To Fetch $RESOURCE Collection', | |||||
unableToFetchResource: 'Unable To Fetch $RESOURCE', | |||||
unableToDeleteResource: 'Unable To Delete $RESOURCE', | |||||
languageNotAcceptable: 'Language Not Acceptable', | |||||
characterSetNotAcceptable: 'Character Set Not Acceptable', | |||||
unableToDeserializeResource: 'Unable To Deserialize $RESOURCE', | |||||
unableToDecodeResource: 'Unable To Decode $RESOURCE', | |||||
mediaTypeNotAcceptable: 'Media Type Not Acceptable', | |||||
methodNotAllowed: 'Method Not Allowed', | |||||
urlNotFound: 'URL Not Found', | |||||
badRequest: 'Bad Request', | |||||
ok: 'OK', | |||||
provideOptions: 'Provide Options', | |||||
resourceCollectionFetched: '$RESOURCE Collection Fetched', | |||||
resourceCollectionQueried: '$RESOURCE Collection Queried', | |||||
resourceFetched: '$RESOURCE Fetched', | |||||
resourceNotFound: '$RESOURCE Not Found', | |||||
deleteNonExistingResource: 'Delete Non-Existing $RESOURCE', | |||||
resourceDeleted: '$RESOURCE Deleted', | |||||
unableToDeserializeRequest: 'Unable To Deserialize Request', | |||||
patchNonExistingResource: 'Patch Non-Existing $RESOURCE', | |||||
unableToPatchResource: 'Unable To Patch $RESOURCE', | |||||
invalidResourcePatch: 'Invalid $RESOURCE Patch', | |||||
invalidResourcePatchType: 'Invalid $RESOURCE Patch Type', | |||||
invalidResource: 'Invalid $RESOURCE', | |||||
resourcePatched: '$RESOURCE Patched', | |||||
resourceCreated: '$RESOURCE Created', | |||||
resourceReplaced: '$RESOURCE Replaced', | |||||
unableToGenerateIdFromResourceDataSource: 'Unable To Generate ID From $RESOURCE Data Source', | |||||
unableToAssignIdFromResourceDataSource: 'Unable To Assign ID From $RESOURCE Data Source', | |||||
unableToEmplaceResource: 'Unable To Emplace $RESOURCE', | |||||
resourceIdNotGiven: '$RESOURCE ID Not Given', | |||||
unableToCreateResource: 'Unable To Create $RESOURCE', | |||||
notImplemented: 'Not Implemented', | |||||
internalServerError: 'Internal Server Error', | |||||
}, | |||||
bodies: { | |||||
badRequest: [ | |||||
'An invalid request has been made.', | |||||
[ | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
'Check if the request is appropriate for this endpoint.', | |||||
], | |||||
], | |||||
languageNotAcceptable: [ | |||||
'The server could not process a response suitable for the client\'s provided language requirement.', | |||||
[ | |||||
'Choose from the available languages on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
characterSetNotAcceptable: [ | |||||
'The server could not process a response suitable for the client\'s provided character set requirement.', | |||||
[ | |||||
'Choose from the available character sets on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
mediaTypeNotAcceptable: [ | |||||
'The server could not process a response suitable for the client\'s provided media type requirement.', | |||||
[ | |||||
'Choose from the available media types on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
deleteNonExistingResource: [ | |||||
'The client has attempted to delete a resource that does not exist.', | |||||
[ | |||||
'Ensure that the resource still exists.', | |||||
'Ensure that the correct method is provided.', | |||||
], | |||||
], | |||||
internalServerError: [ | |||||
'An unknown error has occurred within the service.', | |||||
[ | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator if the service remains in a degraded or non-functional state.', | |||||
], | |||||
], | |||||
invalidResource: [ | |||||
'The request has an invalid structure or is missing some attributes.', | |||||
[ | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
], | |||||
], | |||||
invalidResourcePatch: [ | |||||
'The request has an invalid patch data.', | |||||
[ | |||||
'Check if the appropriate patch type is specified on the request data.', | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
], | |||||
], | |||||
invalidResourcePatchType: [ | |||||
'The request has an invalid or unsupported kind of patch data.', | |||||
[ | |||||
'Check if the appropriate patch type is specified on the request data.', | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
], | |||||
], | |||||
methodNotAllowed: [ | |||||
'A request with an invalid or unsupported method has been made.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Check if the client is authorized to perform the method on this endpoint.', | |||||
] | |||||
], | |||||
notImplemented: [ | |||||
'The service does not have any implementation for the accessed endpoint.', | |||||
[ | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator if the service remains in a degraded or non-functional state.', | |||||
], | |||||
], | |||||
patchNonExistingResource: [ | |||||
'The client has attempted to patch a resource that does not exist.', | |||||
[ | |||||
'Ensure that the resource still exists.', | |||||
'Ensure that the correct method is provided.', | |||||
], | |||||
], | |||||
resourceIdNotGiven: [ | |||||
'The resource ID is not provided for the accessed endpoint.', | |||||
[ | |||||
'Check if the resource ID is provided and valid in the URL.', | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
], | |||||
], | |||||
unableToAssignIdFromResourceDataSource: [ | |||||
'The resource could not be assigned an ID from the associated data source.', | |||||
[ | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToBindResourceDataSource: [ | |||||
'The resource could not be associated from the data source.', | |||||
[ | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToCreateResource: [ | |||||
'An error has occurred on creating the resource.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToDecodeResource: [ | |||||
'The resource byte array could not be decoded for the provided character set.', | |||||
[ | |||||
'Choose from the available character sets on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
unableToDeleteResource: [ | |||||
'An error has occurred on deleting the resource.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToDeserializeRequest: [ | |||||
'The decoded request byte array could not be deserialized for the provided media type.', | |||||
[ | |||||
'Choose from the available media types on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
unableToDeserializeResource: [ | |||||
'The decoded resource could not be deserialized for the provided media type.', | |||||
[ | |||||
'Choose from the available media types on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
unableToEmplaceResource: [ | |||||
'An error has occurred on emplacing the resource.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToEncodeResponse: [ | |||||
'The response data could not be encoded for the provided character set.', | |||||
[ | |||||
'Choose from the available character sets on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
unableToFetchResource: [ | |||||
'An error has occurred on fetching the resource.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToFetchResourceCollection: [ | |||||
'An error has occurred on fetching the resource collection.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToGenerateIdFromResourceDataSource: [ | |||||
'The associated data source for the resource could not produce an ID.', | |||||
[ | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToInitializeResourceDataSource: [ | |||||
'The associated data source for the resource could not be connected for usage.', | |||||
[ | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToPatchResource: [ | |||||
'An error has occurred on patching the resource.', | |||||
[ | |||||
'Check if the request method is appropriate for this endpoint.', | |||||
'Check if the request body has all the required attributes for this endpoint.', | |||||
'Check if the request body has only the valid attributes for this endpoint.', | |||||
'Check if the request body matches the schema for the resource associated with this endpoint.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
unableToSerializeResponse: [ | |||||
'The response data could not be serialized for the provided media type.', | |||||
[ | |||||
'Choose from the available media types on this service.', | |||||
'Contact the administrator to provide localization for the client\'s given requirements.', | |||||
], | |||||
], | |||||
urlNotFound: [ | |||||
'An endpoint in the provided URL could not be found.', | |||||
[ | |||||
'Check if the request URL is correct.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
resourceNotFound: [ | |||||
'The resource in the provided URL could not be found.', | |||||
[ | |||||
'Check if the request URL is correct.', | |||||
'Try the request again at a later time.', | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||||
], | |||||
], | |||||
}, | |||||
} satisfies Language; |
@@ -0,0 +1,37 @@ | |||||
export interface MediaType< | |||||
Name extends string = string, | |||||
T extends object = object, | |||||
SerializeOpts extends {} = {}, | |||||
DeserializeOpts extends {} = {} | |||||
> { | |||||
name: Name; | |||||
serialize: (object: T, args?: SerializeOpts) => string; | |||||
deserialize: (s: string, args?: DeserializeOpts) => T; | |||||
} | |||||
export const FALLBACK_MEDIA_TYPE = { | |||||
serialize: (obj: unknown) => JSON.stringify(obj), | |||||
deserialize: (str: string) => JSON.parse(str), | |||||
name: 'application/json' as const, | |||||
} satisfies MediaType; | |||||
export const PATCH_CONTENT_TYPES = [ | |||||
'application/merge-patch+json', | |||||
'application/json-patch+json', | |||||
] as const; | |||||
export type PatchContentType = typeof PATCH_CONTENT_TYPES[number]; | |||||
export const getAcceptPostString = (mediaTypes: Map<string, MediaType>) => Array.from(mediaTypes.keys()) | |||||
.filter((t) => !PATCH_CONTENT_TYPES.includes(t as PatchContentType)) | |||||
.join(','); | |||||
export const isTextMediaType = (mediaType: string) => ( | |||||
mediaType.startsWith('text/') | |||||
|| [ | |||||
'application/json', | |||||
'application/xml', | |||||
'application/x-www-form-urlencoded', | |||||
...PATCH_CONTENT_TYPES, | |||||
].includes(mediaType) | |||||
); |
@@ -19,28 +19,23 @@ export type MethodWithExtensions = Method | typeof AVAILABLE_EXTENSION_METHODS[n | |||||
export interface BaseOperationParams< | export interface BaseOperationParams< | ||||
Name extends string = string, | Name extends string = string, | ||||
Method extends MethodWithExtensions = MethodWithExtensions, | Method extends MethodWithExtensions = MethodWithExtensions, | ||||
Args extends readonly string[] = readonly string[] | |||||
> { | > { | ||||
name: Name; | name: Name; | ||||
method?: Method; | method?: Method; | ||||
args?: Args; | |||||
} | } | ||||
export interface Operation<Params extends BaseOperationParams = BaseOperationParams> { | export interface Operation<Params extends BaseOperationParams = BaseOperationParams> { | ||||
name: Params['name']; | name: Params['name']; | ||||
method: Params['method']; | method: Params['method']; | ||||
args: Params['args']; | |||||
} | } | ||||
class OperationInstance<Params extends BaseOperationParams = BaseOperationParams> implements Operation<Params> { | class OperationInstance<Params extends BaseOperationParams = BaseOperationParams> implements Operation<Params> { | ||||
readonly name: Params['name']; | readonly name: Params['name']; | ||||
readonly method: Params['method']; | readonly method: Params['method']; | ||||
readonly args: Params['args']; | |||||
constructor(params: Params) { | constructor(params: Params) { | ||||
this.name = params.name; | this.name = params.name; | ||||
this.method = params.method ?? 'GET'; | this.method = params.method ?? 'GET'; | ||||
this.args = params.args; | |||||
} | } | ||||
} | } | ||||
@@ -0,0 +1,64 @@ | |||||
import {ErrorStatusCode, isErrorStatusCode, StatusCode} from './status-codes'; | |||||
import http from 'http'; | |||||
export interface Response { | |||||
statusCode: number; | |||||
statusMessage: string; | |||||
body?: Buffer; | |||||
res?: http.ServerResponse; | |||||
} | |||||
interface ErrorResponse extends Error, Response {} | |||||
interface HttpResponseConstructor<R extends Response> { | |||||
new (...args: any[]): R; | |||||
} | |||||
interface HttpResponseErrorConstructor<R extends Response> extends HttpResponseConstructor<R> { | |||||
new (message?: string, options?: ErrorOptions): R; | |||||
} | |||||
interface HttpSuccessResponseConstructor<R extends Response> extends HttpResponseConstructor<R> { | |||||
new (response: Partial<Omit<Response, 'statusCode' | 'res'>>, options?: Pick<Response, 'res'>): R; | |||||
} | |||||
interface HttpErrorOptions extends ErrorOptions { | |||||
res?: http.ServerResponse; | |||||
body?: Response['body']; | |||||
} | |||||
export const HttpResponse = < | |||||
T extends StatusCode = StatusCode, | |||||
R extends Response = T extends ErrorStatusCode ? ErrorResponse : Response, | |||||
>(statusCode: T): T extends ErrorStatusCode ? HttpResponseErrorConstructor<R> : HttpSuccessResponseConstructor<R> => { | |||||
if (isErrorStatusCode(statusCode)) { | |||||
return class HttpErrorResponse extends Error implements ErrorResponse { | |||||
readonly statusMessage: string; | |||||
readonly statusCode: T; | |||||
readonly res?: http.ServerResponse; | |||||
readonly body?: Buffer; | |||||
constructor(message?: string, options?: HttpErrorOptions) { | |||||
super(message, options); | |||||
this.name = this.statusMessage = message ?? ''; | |||||
this.statusCode = statusCode; | |||||
this.cause = options?.cause; | |||||
this.res = options?.res; | |||||
this.body = options?.body; | |||||
} | |||||
} as unknown as HttpResponseErrorConstructor<R>; | |||||
} | |||||
return class HttpSuccessResponse implements Response { | |||||
readonly statusMessage: string; | |||||
readonly statusCode: T; | |||||
readonly body?: Buffer; | |||||
readonly res?: http.ServerResponse; | |||||
constructor(params: Partial<Omit<Response, 'statusCode'>>, options?: Pick<Response, 'res'>) { | |||||
this.statusCode = statusCode; | |||||
this.statusMessage = params.statusMessage ?? ''; | |||||
this.body = params.body; | |||||
this.res = options?.res; | |||||
} | |||||
} as unknown as HttpSuccessResponseConstructor<R>; | |||||
}; |
@@ -0,0 +1,198 @@ | |||||
export const HTTP_STATUS_CONTINUE = 100 as const; | |||||
export const HTTP_STATUS_SWITCHING_PROTOCOLS = 101 as const; | |||||
export const HTTP_STATUS_PROCESSING = 102 as const; | |||||
export const HTTP_STATUS_EARLY_HINTS = 103 as const; | |||||
export const IN_TRANSIT_STATUS_CODES = [ | |||||
HTTP_STATUS_CONTINUE, | |||||
HTTP_STATUS_SWITCHING_PROTOCOLS, | |||||
HTTP_STATUS_PROCESSING, | |||||
HTTP_STATUS_EARLY_HINTS, | |||||
] as const; | |||||
export type InTransitStatusCode = typeof IN_TRANSIT_STATUS_CODES[number]; | |||||
export const isInTransitStatusCode = (statusCode: StatusCode): statusCode is InTransitStatusCode => ( | |||||
IN_TRANSIT_STATUS_CODES.includes(statusCode as InTransitStatusCode) | |||||
); | |||||
export const HTTP_STATUS_OK = 200 as const; | |||||
export const HTTP_STATUS_CREATED = 201 as const; | |||||
export const HTTP_STATUS_ACCEPTED = 202 as const; | |||||
export const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203 as const; | |||||
export const HTTP_STATUS_NO_CONTENT = 204 as const; | |||||
export const HTTP_STATUS_RESET_CONTENT = 205 as const; | |||||
export const HTTP_STATUS_PARTIAL_CONTENT = 206 as const; | |||||
export const HTTP_STATUS_MULTI_STATUS = 207 as const; | |||||
export const HTTP_STATUS_ALREADY_REPORTED = 208 as const; | |||||
export const HTTP_STATUS_IM_USED = 226 as const; | |||||
export const HTTP_STATUS_MULTIPLE_CHOICES = 300 as const; | |||||
export const HTTP_STATUS_MOVED_PERMANENTLY = 301 as const; | |||||
export const HTTP_STATUS_FOUND = 302 as const; | |||||
export const HTTP_STATUS_SEE_OTHER = 303 as const; | |||||
export const HTTP_STATUS_NOT_MODIFIED = 304 as const; | |||||
export const HTTP_STATUS_USE_PROXY = 305 as const; | |||||
export const HTTP_STATUS_TEMPORARY_REDIRECT = 307 as const; | |||||
export const HTTP_STATUS_PERMANENT_REDIRECT = 308 as const; | |||||
export const RESOLVED_STATUS_CODES = [ | |||||
HTTP_STATUS_OK, | |||||
HTTP_STATUS_CREATED, | |||||
HTTP_STATUS_ACCEPTED, | |||||
HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION, | |||||
HTTP_STATUS_NO_CONTENT, | |||||
HTTP_STATUS_RESET_CONTENT, | |||||
HTTP_STATUS_PARTIAL_CONTENT, | |||||
HTTP_STATUS_MULTI_STATUS, | |||||
HTTP_STATUS_ALREADY_REPORTED, | |||||
HTTP_STATUS_IM_USED, | |||||
] as const; | |||||
export type ResolvedStatusCode = typeof RESOLVED_STATUS_CODES[number]; | |||||
export const isResolvedStatusCode = (statusCode: StatusCode): statusCode is ResolvedStatusCode => ( | |||||
RESOLVED_STATUS_CODES.includes(statusCode as ResolvedStatusCode) | |||||
); | |||||
export const REDIRECT_STATUS_CODES = [ | |||||
HTTP_STATUS_MULTIPLE_CHOICES, | |||||
HTTP_STATUS_MOVED_PERMANENTLY, | |||||
HTTP_STATUS_FOUND, | |||||
HTTP_STATUS_SEE_OTHER, | |||||
HTTP_STATUS_NOT_MODIFIED, | |||||
HTTP_STATUS_USE_PROXY, | |||||
HTTP_STATUS_TEMPORARY_REDIRECT, | |||||
HTTP_STATUS_PERMANENT_REDIRECT, | |||||
] as const; | |||||
export type RedirectStatusCode = typeof REDIRECT_STATUS_CODES[number]; | |||||
export const isRedirectStatusCode = (statusCode: StatusCode): statusCode is RedirectStatusCode => ( | |||||
REDIRECT_STATUS_CODES.includes(statusCode as RedirectStatusCode) | |||||
); | |||||
export const SUCCESS_STATUS_CODES = [ | |||||
...RESOLVED_STATUS_CODES, | |||||
...REDIRECT_STATUS_CODES, | |||||
]; | |||||
export type SuccessStatusCode = typeof SUCCESS_STATUS_CODES[number]; | |||||
export const isSuccessStatusCode = (statusCode: StatusCode): statusCode is SuccessStatusCode => ( | |||||
SUCCESS_STATUS_CODES.includes(statusCode as SuccessStatusCode) | |||||
); | |||||
export const HTTP_STATUS_BAD_REQUEST = 400 as const; | |||||
export const HTTP_STATUS_UNAUTHORIZED = 401 as const; | |||||
export const HTTP_STATUS_PAYMENT_REQUIRED = 402 as const; | |||||
export const HTTP_STATUS_FORBIDDEN = 403 as const; | |||||
export const HTTP_STATUS_NOT_FOUND = 404 as const; | |||||
export const HTTP_STATUS_METHOD_NOT_ALLOWED = 405 as const; | |||||
export const HTTP_STATUS_NOT_ACCEPTABLE = 406 as const; | |||||
export const HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED = 407 as const; | |||||
export const HTTP_STATUS_REQUEST_TIMEOUT = 408 as const; | |||||
export const HTTP_STATUS_CONFLICT = 409 as const; | |||||
export const HTTP_STATUS_GONE = 410 as const; | |||||
export const HTTP_STATUS_LENGTH_REQUIRED = 411 as const; | |||||
export const HTTP_STATUS_PRECONDITION_FAILED = 412 as const; | |||||
export const HTTP_STATUS_PAYLOAD_TOO_LARGE = 413 as const; | |||||
export const HTTP_STATUS_URI_TOO_LONG = 414 as const; | |||||
export const HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415 as const; | |||||
export const HTTP_STATUS_RANGE_NOT_SATISFIABLE = 416 as const; | |||||
export const HTTP_STATUS_EXPECTATION_FAILED = 417 as const; | |||||
export const HTTP_STATUS_TEAPOT = 418 as const; | |||||
export const HTTP_STATUS_MISDIRECTED_REQUEST = 421 as const; | |||||
export const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422 as const; | |||||
export const HTTP_STATUS_LOCKED = 423 as const; | |||||
export const HTTP_STATUS_FAILED_DEPENDENCY = 424 as const; | |||||
export const HTTP_STATUS_UNORDERED_COLLECTION = 425 as const; | |||||
export const HTTP_STATUS_UPGRADE_REQUIRED = 426 as const; | |||||
export const HTTP_STATUS_PRECONDITION_REQUIRED = 428 as const; | |||||
export const HTTP_STATUS_TOO_MANY_REQUESTS = 429 as const; | |||||
export const HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 as const; | |||||
export const HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451 as const; | |||||
export const CLIENT_ERROR_STATUS_CODES = [ | |||||
HTTP_STATUS_BAD_REQUEST, | |||||
HTTP_STATUS_UNAUTHORIZED, | |||||
HTTP_STATUS_PAYMENT_REQUIRED, | |||||
HTTP_STATUS_FORBIDDEN, | |||||
HTTP_STATUS_NOT_FOUND, | |||||
HTTP_STATUS_METHOD_NOT_ALLOWED, | |||||
HTTP_STATUS_NOT_ACCEPTABLE, | |||||
HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED, | |||||
HTTP_STATUS_REQUEST_TIMEOUT, | |||||
HTTP_STATUS_CONFLICT, | |||||
HTTP_STATUS_GONE, | |||||
HTTP_STATUS_LENGTH_REQUIRED, | |||||
HTTP_STATUS_PRECONDITION_FAILED, | |||||
HTTP_STATUS_PAYLOAD_TOO_LARGE, | |||||
HTTP_STATUS_URI_TOO_LONG, | |||||
HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, | |||||
HTTP_STATUS_RANGE_NOT_SATISFIABLE, | |||||
HTTP_STATUS_EXPECTATION_FAILED, | |||||
HTTP_STATUS_TEAPOT, | |||||
HTTP_STATUS_MISDIRECTED_REQUEST, | |||||
HTTP_STATUS_UNPROCESSABLE_ENTITY, | |||||
HTTP_STATUS_LOCKED, | |||||
HTTP_STATUS_FAILED_DEPENDENCY, | |||||
HTTP_STATUS_UNORDERED_COLLECTION, | |||||
HTTP_STATUS_UPGRADE_REQUIRED, | |||||
HTTP_STATUS_PRECONDITION_REQUIRED, | |||||
HTTP_STATUS_TOO_MANY_REQUESTS, | |||||
HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE, | |||||
HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS, | |||||
] as const; | |||||
export type ClientErrorStatusCode = typeof CLIENT_ERROR_STATUS_CODES[number]; | |||||
export const isClientErrorStatusCode = (statusCode: StatusCode): statusCode is ClientErrorStatusCode => ( | |||||
CLIENT_ERROR_STATUS_CODES.includes(statusCode as ClientErrorStatusCode) | |||||
); | |||||
export const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500 as const; | |||||
export const HTTP_STATUS_NOT_IMPLEMENTED = 501 as const; | |||||
export const HTTP_STATUS_BAD_GATEWAY = 502 as const; | |||||
export const HTTP_STATUS_SERVICE_UNAVAILABLE = 503 as const; | |||||
export const HTTP_STATUS_GATEWAY_TIMEOUT = 504 as const; | |||||
export const HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED = 505 as const; | |||||
export const HTTP_STATUS_VARIANT_ALSO_NEGOTIATES = 506 as const; | |||||
export const HTTP_STATUS_INSUFFICIENT_STORAGE = 507 as const; | |||||
export const HTTP_STATUS_LOOP_DETECTED = 508 as const; | |||||
export const HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED = 509 as const; | |||||
export const HTTP_STATUS_NOT_EXTENDED = 510 as const; | |||||
export const HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511 as const; | |||||
export const SERVER_ERROR_STATUS_CODES = [ | |||||
HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||||
HTTP_STATUS_NOT_IMPLEMENTED, | |||||
HTTP_STATUS_BAD_GATEWAY, | |||||
HTTP_STATUS_SERVICE_UNAVAILABLE, | |||||
HTTP_STATUS_GATEWAY_TIMEOUT, | |||||
HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED, | |||||
HTTP_STATUS_VARIANT_ALSO_NEGOTIATES, | |||||
HTTP_STATUS_INSUFFICIENT_STORAGE, | |||||
HTTP_STATUS_LOOP_DETECTED, | |||||
HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED, | |||||
HTTP_STATUS_NOT_EXTENDED, | |||||
HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED, | |||||
] as const; | |||||
export type ServerErrorStatusCode = typeof SERVER_ERROR_STATUS_CODES[number]; | |||||
export const isServerErrorStatusCode = (statusCode: StatusCode): statusCode is ServerErrorStatusCode => ( | |||||
SERVER_ERROR_STATUS_CODES.includes(statusCode as ServerErrorStatusCode) | |||||
); | |||||
export const ERROR_STATUS_CODES = [ | |||||
...CLIENT_ERROR_STATUS_CODES, | |||||
...SERVER_ERROR_STATUS_CODES, | |||||
] as const; | |||||
export type ErrorStatusCode = typeof ERROR_STATUS_CODES[number]; | |||||
export const isErrorStatusCode = (statusCode: StatusCode): statusCode is ErrorStatusCode => ( | |||||
ERROR_STATUS_CODES.includes(statusCode as ErrorStatusCode) | |||||
); | |||||
export type StatusCode = InTransitStatusCode | SuccessStatusCode | ErrorStatusCode; |
@@ -1,16 +1,34 @@ | |||||
import {describe, it, expect, beforeAll} from 'vitest'; | |||||
import {App, app, Endpoint, endpoint, Operation, operation, validation as v} from '../src/common'; | |||||
import {describe, it, expect, beforeAll, afterAll} from 'vitest'; | |||||
import {App, app, DataSource, Endpoint, endpoint, Operation, operation, validation as v} from '../src/common'; | |||||
import {server} from '../src/extenders/http/backend'; | import {server} from '../src/extenders/http/backend'; | ||||
import {Backend, backend, Server} from '../src/backend'; | import {Backend, backend, Server} from '../src/backend'; | ||||
import {Client} from '../src/client'; | import {Client} from '../src/client'; | ||||
import {client} from '../src/extenders/http/client'; | import {client} from '../src/extenders/http/client'; | ||||
const op = operation({ | |||||
name: 'create' as const, | |||||
method: 'POST' as const, | |||||
}); | |||||
const e = endpoint({ | |||||
name: 'e' as const, | |||||
schema: v.object({}), | |||||
}) | |||||
.can('create'); | |||||
const a = app({ | |||||
name: 'foo' as const, | |||||
}) | |||||
.operation(op) | |||||
.endpoint(e); | |||||
describe('app', () => { | describe('app', () => { | ||||
let theApp: App; | let theApp: App; | ||||
let theBackend: Backend; | let theBackend: Backend; | ||||
let theClient: Client; | |||||
let theDataSource: DataSource; | |||||
let theEndpoint: Endpoint; | let theEndpoint: Endpoint; | ||||
let theServer: Server; | let theServer: Server; | ||||
let theClient: Client<App>; | |||||
let theOperation: Operation; | let theOperation: Operation; | ||||
beforeAll(async () => { | beforeAll(async () => { | ||||
@@ -61,6 +79,10 @@ describe('app', () => { | |||||
.connect(connectionParams); | .connect(connectionParams); | ||||
}); | }); | ||||
afterAll(() => { | |||||
theServer.close(); | |||||
}); | |||||
it('works', async () => { | it('works', async () => { | ||||
const response = await theClient | const response = await theClient | ||||
.at(theEndpoint, { resourceId: 3 }) | .at(theEndpoint, { resourceId: 3 }) | ||||