diff --git a/TODO.md b/TODO.md index 377a333..c1c4e50 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ - [X] Language - [X] Charset - [X] Media Type - - [ ] Improve content negotiation on success/error responses (able to explicitly select language/media type/charset) + - [X] Improve content negotiation on success/error responses (able to explicitly select language/media type/charset) - [X] HTTPS - [X] Date/Datetime handling (endpoints should be able to accept timestamps and ISO date/datetime strings) - [ ] Querying items in collections @@ -20,6 +20,8 @@ - [ ] Tests - [X] Happy path - [ ] Error handling + - [X] Resource handlers + - [ ] Generic errors - [ ] Implement error handling compliant to RFC 9457 - Problem Details for HTTP APIs - [ ] Create RESTful client for frontend, server for backend (specify data sources on the server side) - [ ] `EventEmitter` for `202 Accepted` requests (CQRS-style service) diff --git a/packages/core/src/backend/index.ts b/packages/core/src/backend/index.ts index 1243f85..a2abc7e 100644 --- a/packages/core/src/backend/index.ts +++ b/packages/core/src/backend/index.ts @@ -1,3 +1,5 @@ export * from './core'; export * from './common'; export * from './data-source'; + +export * as http from './servers/http'; diff --git a/packages/core/src/backend/servers/http/core.ts b/packages/core/src/backend/servers/http/core.ts index 4b75281..68bac98 100644 --- a/packages/core/src/backend/servers/http/core.ts +++ b/packages/core/src/backend/servers/http/core.ts @@ -6,13 +6,15 @@ import { AllowedMiddlewareSpecification, BackendState, Middleware, - RequestContext, RequestDecorator, + RequestContext, + RequestDecorator, Response, } from '../../common'; import { BaseResourceType, CanPatchSpec, DELTA_SCHEMA, + LanguageDefaultErrorStatusMessageKey, PATCH_CONTENT_MAP_TYPE, PatchContentType, Resource, @@ -160,11 +162,11 @@ class CqrsEventEmitter extends EventEmitter { } -export type ErrorHandler = (err?: E) => never; +export type ErrorHandler = (req: RequestContext, res: http.ServerResponse) => (err?: E) => never; interface ServerState { requestDecorators: Set; - errorHandler: ErrorHandler; + defaultErrorHandler?: ErrorHandler; } export interface Server { @@ -176,14 +178,10 @@ export interface Server { defaultErrorHandler(errorHandler: ErrorHandler): this; } -const DEFAULT_ERROR_HANDLER: ErrorHandler = (err) => { - throw err; -}; - export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { const state: ServerState = { requestDecorators: new Set(), - errorHandler: DEFAULT_ERROR_HANDLER, + defaultErrorHandler: undefined, }; const isHttps = 'key' in serverParams && 'cert' in serverParams; @@ -407,14 +405,16 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset; let encoded: Buffer | undefined; let serialized; + + const body = finalErr.body ?? language.bodies[(finalErr.statusMessage ?? 'internalServerError') as LanguageDefaultErrorStatusMessageKey]; try { - serialized = typeof finalErr.body !== 'undefined' ? mediaType.serialize(finalErr.body) : undefined; + serialized = mediaType.serialize(body); } catch (cause) { res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace( /\$RESOURCE/g, resourceReq.resource.state.itemName) ?? ''; res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); - res.end(); + res.end(language.bodies['unableToSerializeResponse']); return; } @@ -424,7 +424,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr res.statusMessage = language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g, resourceReq.resource.state.itemName) ?? ''; res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); - res.end(); + res.end(language.bodies['unableToEncodeResponse']); return; } @@ -450,7 +450,53 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr const handleError = (err: Error) => (req: RequestContext, res: http.ServerResponse) => { if ('resource' in req && typeof req.resource !== 'undefined') { handleResourceError(err)(req as ResourceRequestContext, res); + return; + } + + const finalErr = err as ErrorPlainResponse; + const headers = finalErr.headers ?? {}; + const language = req.backend.cn.language; + const mediaType = req.backend.cn.mediaType; + const charset = req.backend.cn.charset; + + let encoded: Buffer | undefined; + let serialized; + const body = finalErr.body ?? language.bodies[(finalErr.statusMessage ?? 'internalServerError') as LanguageDefaultErrorStatusMessageKey]; + try { + serialized = mediaType.serialize(body); + } catch (cause) { + res.statusMessage = language.statusMessages['unableToSerializeResponse']; + res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); + res.end(); + return; + } + + try { + encoded = typeof serialized !== 'undefined' ? charset.encode(serialized) : undefined; + } catch (cause) { + res.statusMessage = language.statusMessages['unableToEncodeResponse']; + res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); + res.end(); + return; + } + + headers['Content-Type'] = [ + mediaType.name, + typeof serialized !== 'undefined' ? `charset=${charset.name}` : '', + ] + .filter((s) => s.length > 0) + .join('; '); + + res.statusMessage = typeof finalErr.statusMessage !== 'undefined' ? language.statusMessages[finalErr.statusMessage] : ''; + res.writeHead( + finalErr.statusCode ?? constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, + headers, + ) + if (typeof encoded !== 'undefined') { + res.end(encoded); + return; } + res.end(); }; const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse) => { @@ -573,18 +619,17 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr } try { - state.errorHandler(); + state.defaultErrorHandler?.(reqRaw, res)(); } catch (err) { handleError(err as Error)(reqRaw, res); - //handleMiddlewareError(err)(reqRaw, res); return; } - // TODO default error handling handleError( - new ErrorPlainResponse('urlNotFound', { - statusCode: constants.HTTP_STATUS_NOT_FOUND, + new ErrorPlainResponse('internalServerError', { + statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, res, + body: language.bodies['internalServerError'], }) )(reqRaw, res); }; @@ -610,7 +655,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr return this; }, defaultErrorHandler(errorHandler: ErrorHandler) { - state.errorHandler = errorHandler; + state.defaultErrorHandler = errorHandler; return this; } } satisfies Server; diff --git a/packages/core/src/common/language.ts b/packages/core/src/common/language.ts index 371cdf5..698db74 100644 --- a/packages/core/src/common/language.ts +++ b/packages/core/src/common/language.ts @@ -1,20 +1,16 @@ export type MessageBody = string | string[] | (string | string[])[]; -export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ +export const LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS = [ 'unableToInitializeResourceDataSource', 'unableToFetchResourceCollection', 'unableToFetchResource', 'resourceIdNotGiven', 'languageNotAcceptable', - 'encodingNotAcceptable', + 'characterSetNotAcceptable', 'mediaTypeNotAcceptable', 'methodNotAllowed', 'urlNotFound', 'badRequest', - 'ok', - 'resourceCollectionFetched', - 'resourceFetched', - 'resourceNotFound', 'deleteNonExistingResource', 'unableToCreateResource', 'unableToBindResourceDataSource', @@ -26,34 +22,36 @@ export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ 'unableToDeleteResource', 'unableToDeserializeResource', 'unableToDecodeResource', - 'resourceDeleted', 'unableToDeserializeRequest', 'patchNonExistingResource', 'unableToPatchResource', 'invalidResourcePatch', 'invalidResourcePatchType', 'invalidResource', + 'notImplemented', + 'internalServerError', +] as const; + +export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ + ...LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS, + 'ok', + 'resourceCollectionFetched', + 'resourceFetched', + 'resourceNotFound', + 'resourceDeleted', 'resourcePatched', 'resourceCreated', 'resourceReplaced', - 'notImplemented', 'provideOptions', - 'internalServerError', ] as const; export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number]; export interface LanguageStatusMessageMap extends Record {} -export const LANGUAGE_DEFAULT_BODY_KEYS = [ - 'languageNotAcceptable', - 'encodingNotAcceptable', - 'mediaTypeNotAcceptable', -] as const; - -export type LanguageDefaultBodyKey = typeof LANGUAGE_DEFAULT_BODY_KEYS[number]; +export type LanguageDefaultErrorStatusMessageKey = typeof LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS[number]; -export interface LanguageBodyMap extends Record {} +export interface LanguageBodyMap extends Record {} export interface Language { name: Name, @@ -72,7 +70,7 @@ export const FALLBACK_LANGUAGE = { unableToFetchResource: 'Unable To Fetch $RESOURCE', unableToDeleteResource: 'Unable To Delete $RESOURCE', languageNotAcceptable: 'Language Not Acceptable', - encodingNotAcceptable: 'Encoding Not Acceptable', + characterSetNotAcceptable: 'Character Set Not Acceptable', unableToDeserializeResource: 'Unable To Deserialize $RESOURCE', unableToDecodeResource: 'Unable To Decode $RESOURCE', mediaTypeNotAcceptable: 'Media Type Not Acceptable', @@ -104,8 +102,231 @@ export const FALLBACK_LANGUAGE = { internalServerError: 'Internal Server Error', }, bodies: { - languageNotAcceptable: [], - encodingNotAcceptable: [], - mediaTypeNotAcceptable: [] + 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.', + ], + ], }, } satisfies Language; diff --git a/packages/core/test/utils.ts b/packages/core/test/utils.ts index 5fb159d..4c8003a 100644 --- a/packages/core/test/utils.ts +++ b/packages/core/test/utils.ts @@ -195,48 +195,271 @@ export class DummyDataSource implements DataSource { export const TEST_LANGUAGE: Language = { name: FALLBACK_LANGUAGE.name, statusMessages: { - unableToInitializeResourceDataSource: 'unableToInitializeResourceDataSource', - unableToFetchResourceCollection: 'unableToFetchResourceCollection', - unableToFetchResource: 'unableToFetchResource', - resourceIdNotGiven: 'resourceIdNotGiven', - languageNotAcceptable: 'languageNotAcceptable', - encodingNotAcceptable: 'encodingNotAcceptable', - mediaTypeNotAcceptable: 'mediaTypeNotAcceptable', - methodNotAllowed: 'methodNotAllowed', - urlNotFound: 'urlNotFound', - badRequest: 'badRequest', - ok: 'ok', - resourceCollectionFetched: 'resourceCollectionFetched', - resourceFetched: 'resourceFetched', - resourceNotFound: 'resourceNotFound', - deleteNonExistingResource: 'deleteNonExistingResource', - unableToCreateResource: 'unableToCreateResource', - unableToBindResourceDataSource: 'unableToBindResourceDataSource', - unableToGenerateIdFromResourceDataSource: 'unableToGenerateIdFromResourceDataSource', - unableToAssignIdFromResourceDataSource: 'unableToAssignIdFromResourceDataSource', - unableToEmplaceResource: 'unableToEmplaceResource', - unableToSerializeResponse: 'unableToSerializeResponse', - unableToEncodeResponse: 'unableToEncodeResponse', - unableToDeleteResource: 'unableToDeleteResource', - unableToDeserializeResource: 'unableToDeserializeResource', - unableToDecodeResource: 'unableToDecodeResource', - resourceDeleted: 'resourceDeleted', - unableToDeserializeRequest: 'unableToDeserializeRequest', - patchNonExistingResource: 'patchNonExistingResource', - unableToPatchResource: 'unableToPatchResource', - invalidResourcePatch: 'invalidResourcePatch', - invalidResourcePatchType: 'invalidResourcePatchType', - invalidResource: 'invalidResource', - resourcePatched: 'resourcePatched', - resourceCreated: 'resourceCreated', - resourceReplaced: 'resourceReplaced', - notImplemented: 'notImplemented', - provideOptions: 'provideOptions', - internalServerError: 'internalServerError', + 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', + 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: { - languageNotAcceptable: [], - encodingNotAcceptable: [], - mediaTypeNotAcceptable: [], + 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.', + ], + ], }, }; diff --git a/packages/examples/cms-web-api/src/index.ts b/packages/examples/cms-web-api/src/index.ts index 759a79c..880c0a5 100644 --- a/packages/examples/cms-web-api/src/index.ts +++ b/packages/examples/cms-web-api/src/index.ts @@ -1,6 +1,15 @@ import { application, resource, validation as v } from '@modal-sh/yasumi'; +import { http } from '@modal-sh/yasumi/backend'; import { randomUUID } from 'crypto'; import {JsonLinesDataSource} from '@modal-sh/yasumi-data-source-file-jsonl'; +import { constants } from 'http2'; + +const UuidIdConfig = { + generationStrategy: () => Promise.resolve(randomUUID()), + schema: v.string(), + serialize: (v: unknown) => v?.toString() ?? '', + deserialize: (v: string) => v, // TODO resolve bytes/non-formatted UUIDs +}; const User = resource( v.object({ @@ -12,12 +21,7 @@ const User = resource( ) .name('User') .route('users') - .id('id', { - generationStrategy: () => Promise.resolve(randomUUID()), - schema: v.string(), - serialize: (v: unknown) => v?.toString() ?? '', - deserialize: (v: string) => v, - }) + .id('id', UuidIdConfig) .canFetchItem() .canFetchCollection() .canCreate() @@ -33,12 +37,7 @@ const Post = resource( ) .name('Post') .route('posts') - .id('id', { - generationStrategy: () => Promise.resolve(randomUUID()), - schema: v.string(), - serialize: (v: unknown) => v?.toString() ?? '', - deserialize: (v: string) => v, - }) + .id('id', UuidIdConfig) .createdAt('createdAt') .updatedAt('updatedAt') .canFetchItem() @@ -61,6 +60,16 @@ const backend = app.createBackend({ const server = backend.createHttpServer({ basePath: '/api', -}); +}) + .defaultErrorHandler((_req, res) => () => { + throw new http.ErrorPlainResponse('urlNotFound', { + statusCode: constants.HTTP_STATUS_NOT_FOUND, + res, + }); + // throw new http.ErrorPlainResponse('notImplemented', { + // statusCode: constants.HTTP_STATUS_NOT_IMPLEMENTED, + // res, + // }); + }); server.listen(6969); diff --git a/packages/examples/cms-web-api/test/index.test.ts b/packages/examples/cms-web-api/test/index.test.ts deleted file mode 100644 index 441ca94..0000000 --- a/packages/examples/cms-web-api/test/index.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import add from '../src'; - -describe('blah', () => { - it('works', () => { - expect(add(1, 1)).toEqual(2); - }); -});