Browse Source

Add error bodies

Include basic description of the error and the steps to resolve them.
master
TheoryOfNekomata 8 months ago
parent
commit
0e4d44b764
7 changed files with 595 additions and 101 deletions
  1. +3
    -1
      TODO.md
  2. +2
    -0
      packages/core/src/backend/index.ts
  3. +62
    -17
      packages/core/src/backend/servers/http/core.ts
  4. +242
    -21
      packages/core/src/common/language.ts
  5. +264
    -41
      packages/core/test/utils.ts
  6. +22
    -13
      packages/examples/cms-web-api/src/index.ts
  7. +0
    -8
      packages/examples/cms-web-api/test/index.test.ts

+ 3
- 1
TODO.md View File

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


+ 2
- 0
packages/core/src/backend/index.ts View File

@@ -1,3 +1,5 @@
export * from './core';
export * from './common';
export * from './data-source';

export * as http from './servers/http';

+ 62
- 17
packages/core/src/backend/servers/http/core.ts View File

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


+ 242
- 21
packages/core/src/common/language.ts View File

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

+ 264
- 41
packages/core/test/utils.ts View File

@@ -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.',
],
],
},
};

+ 22
- 13
packages/examples/cms-web-api/src/index.ts View File

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

+ 0
- 8
packages/examples/cms-web-api/test/index.test.ts View File

@@ -1,8 +0,0 @@
import { describe, it, expect } from 'vitest';
import add from '../src';

describe('blah', () => {
it('works', () => {
expect(add(1, 1)).toEqual(2);
});
});

Loading…
Cancel
Save