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