@@ -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) | |||
@@ -1,3 +1,5 @@ | |||
export * from './core'; | |||
export * from './common'; | |||
export * from './data-source'; | |||
export * as http from './servers/http'; |
@@ -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 = <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 { | |||
requestDecorators: Set<RequestDecorator>; | |||
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<RequestDecorator>(), | |||
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<RequestContext>) => { | |||
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<RequestContext>) => { | |||
@@ -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; | |||
@@ -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<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> { | |||
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; |
@@ -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.', | |||
], | |||
], | |||
}, | |||
}; |
@@ -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); |
@@ -1,8 +0,0 @@ | |||
import { describe, it, expect } from 'vitest'; | |||
import add from '../src'; | |||
describe('blah', () => { | |||
it('works', () => { | |||
expect(add(1, 1)).toEqual(2); | |||
}); | |||
}); |