Browse Source

Add HTTP-related exports

Bring back content negotiation exports as well as define a new response
type system.
refactor/new-arch
TheoryOfNekomata 7 months ago
parent
commit
0d3f10b08e
12 changed files with 690 additions and 32 deletions
  1. +5
    -0
      packages/core/src/backend/common.ts
  2. +1
    -1
      packages/core/src/client/index.ts
  3. +3
    -23
      packages/core/src/common/app.ts
  4. +11
    -0
      packages/core/src/common/charset.ts
  5. +1
    -0
      packages/core/src/common/endpoint.ts
  6. +3
    -0
      packages/core/src/common/index.ts
  7. +342
    -0
      packages/core/src/common/language.ts
  8. +37
    -0
      packages/core/src/common/media-type.ts
  9. +0
    -5
      packages/core/src/common/operation.ts
  10. +64
    -0
      packages/core/src/extenders/http/response.ts
  11. +198
    -0
      packages/core/src/extenders/http/status-codes.ts
  12. +25
    -3
      packages/core/test/index.test.ts

+ 5
- 0
packages/core/src/backend/common.ts View File

@@ -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() {
}
}

+ 1
- 1
packages/core/src/client/index.ts View File

@@ -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;


+ 3
- 23
packages/core/src/common/app.ts View File

@@ -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<


+ 11
- 0
packages/core/src/common/charset.ts View File

@@ -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;

+ 1
- 0
packages/core/src/common/endpoint.ts View File

@@ -17,6 +17,7 @@ export const serializeEndpointQueue = (endpointQueue: EndpointQueue) => {
.map((s) => `/${s}`)
.join('');
})
.join('')
};

export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: Set<Endpoint>) => {


+ 3
- 0
packages/core/src/common/index.ts View File

@@ -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';

+ 342
- 0
packages/core/src/common/language.ts View File

@@ -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;

+ 37
- 0
packages/core/src/common/media-type.ts View File

@@ -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)
);

+ 0
- 5
packages/core/src/common/operation.ts View File

@@ -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;
}
}



+ 64
- 0
packages/core/src/extenders/http/response.ts View File

@@ -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>;
};

+ 198
- 0
packages/core/src/extenders/http/status-codes.ts View File

@@ -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;

+ 25
- 3
packages/core/test/index.test.ts View File

@@ -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 })


Loading…
Cancel
Save