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> => { | |||
return new BackendInstance(params); | |||
}; | |||
export class PlainResponse { | |||
constructor() { | |||
} | |||
} |
@@ -5,7 +5,7 @@ export interface ClientParams<App extends BaseApp> { | |||
fetch?: typeof fetch; | |||
} | |||
export interface Client<App extends BaseApp> { | |||
export interface Client<App extends BaseApp = BaseApp> { | |||
app: App; | |||
connect(params: ServiceParams): this; | |||
at<TheEndpoint extends Endpoint = Endpoint>(endpoint: TheEndpoint, params?: Record<GetEndpointParams<TheEndpoint>, unknown>): this; | |||
@@ -1,5 +1,5 @@ | |||
import {Endpoint, EndpointOperations} from './endpoint'; | |||
import {BaseOperationParams, Operation} from './operation'; | |||
import {Operation} from './operation'; | |||
export interface BaseAppState { | |||
endpoints: unknown; | |||
@@ -21,31 +21,11 @@ export interface App<AppName extends string = string, AppState extends BaseAppSt | |||
name: AppName; | |||
operations: Set<Operation>; | |||
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, | |||
{ | |||
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< | |||
@@ -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}`) | |||
.join(''); | |||
}) | |||
.join('') | |||
}; | |||
export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: Set<Endpoint>) => { | |||
@@ -1,6 +1,9 @@ | |||
export * from './app'; | |||
export * from './charset'; | |||
export * from './data-source'; | |||
export * from './endpoint'; | |||
export * from './language'; | |||
export * from './media-type'; | |||
export * from './operation'; | |||
export * from './service'; | |||
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< | |||
Name extends string = string, | |||
Method extends MethodWithExtensions = MethodWithExtensions, | |||
Args extends readonly string[] = readonly string[] | |||
> { | |||
name: Name; | |||
method?: Method; | |||
args?: Args; | |||
} | |||
export interface Operation<Params extends BaseOperationParams = BaseOperationParams> { | |||
name: Params['name']; | |||
method: Params['method']; | |||
args: Params['args']; | |||
} | |||
class OperationInstance<Params extends BaseOperationParams = BaseOperationParams> implements Operation<Params> { | |||
readonly name: Params['name']; | |||
readonly method: Params['method']; | |||
readonly args: Params['args']; | |||
constructor(params: Params) { | |||
this.name = params.name; | |||
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 {Backend, backend, Server} from '../src/backend'; | |||
import {Client} from '../src/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', () => { | |||
let theApp: App; | |||
let theBackend: Backend; | |||
let theClient: Client; | |||
let theDataSource: DataSource; | |||
let theEndpoint: Endpoint; | |||
let theServer: Server; | |||
let theClient: Client<App>; | |||
let theOperation: Operation; | |||
beforeAll(async () => { | |||
@@ -61,6 +79,10 @@ describe('app', () => { | |||
.connect(connectionParams); | |||
}); | |||
afterAll(() => { | |||
theServer.close(); | |||
}); | |||
it('works', async () => { | |||
const response = await theClient | |||
.at(theEndpoint, { resourceId: 3 }) | |||