diff --git a/README.md b/README.md index 8fea3bd..39bf94e 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,37 @@ This is a proposed solution to most Web frameworks that will hopefully inspire other solutions. See [docs folder](./docs) for more details. + +## Links + +- Roy Fielding (creator of REST)'s post about criticisms of REST APIs + + https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven + +- Wikipedia entry on HATEOAS + + https://en.wikipedia.org/wiki/HATEOAS + +- JSON for Linked Data (JSON-LD) Specifications + + https://www.w3.org/TR/json-ld-api/ + +- JSON Hypertext Application Language (HAL) Specifications + + https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-11 + +- Valibot (zod alternative) + + https://valibot.dev/ + +- Content negotiation + + https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation + +- RFC 9457 - Problem Details for HTTP APIs + + https://www.rfc-editor.org/rfc/rfc9457.html + +- RFC 5988 - Web Linking + + https://datatracker.ietf.org/doc/html/rfc5988 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..8e5ba24 --- /dev/null +++ b/TODO.md @@ -0,0 +1,15 @@ +- [ ] Integrate with other data stores +- [X] Access control with resources +- [ ] Content negotiation + - [ ] Language + - [X] Encoding + - [ ] Media Type +- [X] HTTPS +- [X] Date/Datetime handling (endpoints should be able to accept timestamps and ISO date/datetime strings) +- [ ] Querying items in collections +- [ ] Tests + - [X] Happy path + - [ ] Error handling +- [ ] Implement error handling compliant to RFC 9457 - Problem Details for HTTP APIs +- [ ] Create RESTful client for frontend, server for backend (specify data sources on the server side) +- [ ] `EventEmitter` for `202 Accepted` requests (CQRS-style service) diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000..003b8e1 --- /dev/null +++ b/src/common.ts @@ -0,0 +1,44 @@ +import {Resource} from './core'; + +export type MessageBody = string | string[] | (string | string[])[]; + +export interface LanguageStatusMessageMap { + unableToInitializeResourceDataSource(resource: Resource): string; + unableToFetchResourceCollection(resource: Resource): string; + unableToFetchResource(resource: Resource): string; + languageNotAcceptable(): string; + encodingNotAcceptable(): string; + mediaTypeNotAcceptable(): string; + methodNotAllowed(): string; + urlNotFound(): string; + badRequest(): string; + ok(): string; + resourceCollectionFetched(resource: Resource): string; + resourceFetched(resource: Resource): string; + resourceNotFound(resource: Resource): string; + deleteNonExistingResource(resource: Resource): string; + unableToGenerateIdFromResourceDataSource(resource: Resource): string; + unableToEmplaceResource(resource: Resource): string; + unableToSerializeResponse(): string; + unableToEncodeResponse(): string; + unableToDeleteResource(resource: Resource): string; + resourceDeleted(resource: Resource): string; + unableToDeserializeRequest(): string; + patchNonExistingResource(resource: Resource): string; + unableToPatchResource(resource: Resource): string; + invalidResourcePatch(resource: Resource): string; + invalidResource(resource: Resource): string; + resourcePatched(resource: Resource): string; + resourceCreated(resource: Resource): string; + resourceReplaced(resource: Resource): string; +} + +export interface Language { + name: string, + statusMessages: LanguageStatusMessageMap, + bodies: { + languageNotAcceptable(): MessageBody; + encodingNotAcceptable(): MessageBody; + mediaTypeNotAcceptable(): MessageBody; + } +} diff --git a/src/core.ts b/src/core.ts index f86286d..85902a7 100644 --- a/src/core.ts +++ b/src/core.ts @@ -17,6 +17,7 @@ import {EncodingPair} from './encodings'; import * as en from './languages/en'; import * as utf8 from './encodings/utf-8'; import * as applicationJson from './serializers/application/json'; +import {Language} from './common'; // TODO separate frontend and backend factory methods @@ -223,14 +224,6 @@ export const resource = < } as Resource; }; -interface CreateServerParams { - baseUrl?: string; - host?: string; - cert?: string; - key?: string; - requestTimeout?: number; -} - type RequestListenerWithReturn< P extends unknown = unknown, Q extends typeof http.IncomingMessage = typeof http.IncomingMessage, @@ -245,51 +238,11 @@ interface HandlerState { export interface ApplicationState { resources: Set>; - languages: Map; + languages: Set; serializers: Map; encodings: Map; } -type MessageBody = string | string[] | (string | string[])[]; - -export interface MessageCollection { - statusMessages: { - unableToInitializeResourceDataSource(resource: Resource): string; - unableToFetchResourceCollection(resource: Resource): string; - unableToFetchResource(resource: Resource): string; - languageNotAcceptable(): string; - encodingNotAcceptable(): string; - mediaTypeNotAcceptable(): string; - methodNotAllowed(): string; - urlNotFound(): string; - badRequest(): string; - ok(): string; - resourceCollectionFetched(resource: Resource): string; - resourceFetched(resource: Resource): string; - resourceNotFound(resource: Resource): string; - deleteNonExistingResource(resource: Resource): string; - unableToGenerateIdFromResourceDataSource(resource: Resource): string; - unableToEmplaceResource(resource: Resource): string; - unableToSerializeResponse(): string; - unableToEncodeResponse(): string; - unableToDeleteResource(resource: Resource): string; - resourceDeleted(resource: Resource): string; - unableToDeserializeRequest(): string; - patchNonExistingResource(resource: Resource): string; - unableToPatchResource(resource: Resource): string; - invalidResourcePatch(resource: Resource): string; - invalidResource(resource: Resource): string; - resourcePatched(resource: Resource): string; - resourceCreated(resource: Resource): string; - resourceReplaced(resource: Resource): string; - }, - bodies: { - languageNotAcceptable(): MessageBody; - encodingNotAcceptable(): MessageBody; - mediaTypeNotAcceptable(): MessageBody; - } -} - export interface BackendState { fallback: { language: string; @@ -313,10 +266,10 @@ interface MiddlewareArgs { appState: ApplicationState; appParams: ApplicationParams; serverParams: CreateServerParams; - responseBodyLanguage: [string, MessageCollection]; + responseBodyLanguage: Language; responseBodyEncoding: [string, EncodingPair]; responseBodyMediaType: [string, SerializerPair]; - errorResponseBodyLanguage: [string, MessageCollection]; + errorResponseBodyLanguage: Language; errorResponseBodyEncoding: [string, EncodingPair]; errorResponseBodyMediaType: [string, SerializerPair]; resource: ResourceWithDataSource; @@ -328,6 +281,14 @@ export interface Middleware { (args: MiddlewareArgs): RequestListenerWithReturn> } +interface CreateServerParams { + baseUrl?: string; + host?: string; + cert?: string; + key?: string; + requestTimeout?: number; +} + export interface Backend { showTotalItemCountOnGetCollection(b?: boolean): this; showTotalItemCountOnCreateItem(b?: boolean): this; @@ -344,7 +305,7 @@ export interface Client { export interface Application { contentType(mimeTypePrefix: string, serializerPair: SerializerPair): this; - language(languageCode: string, messageCollection: MessageCollection): this; + language(language: Language): this; encoding(encoding: string, encodingPair: EncodingPair): this; resource(resRaw: Partial>): this; createBackend(): Backend; @@ -354,12 +315,12 @@ export interface Application { export const application = (appParams: ApplicationParams): Application => { const appState: ApplicationState = { resources: new Set>(), - languages: new Map(), + languages: new Set(), serializers: new Map(), encodings: new Map(), }; - appState.languages.set(en.name, en.messages); + appState.languages.add(en); appState.encodings.set(utf8.name, utf8); appState.serializers.set(applicationJson.name, applicationJson); @@ -372,8 +333,8 @@ export const application = (appParams: ApplicationParams): Application => { appState.encodings.set(encoding, encodingPair); return this; }, - language(languageCode: string, messageCollection: MessageCollection) { - appState.languages.set(languageCode, messageCollection); + language(language: Language) { + appState.languages.add(language); return this; }, resource(resRaw: Partial>) { @@ -389,7 +350,7 @@ export const application = (appParams: ApplicationParams): Application => { const clientState = { contentType: applicationJson.name, encoding: utf8.name, - language: en.name + language: en.name as string }; return { @@ -461,17 +422,17 @@ export const application = (appParams: ApplicationParams): Application => { const { url, query } = getUrl(req, baseUrl); const negotiator = new Negotiator(req); - const languageCandidate = negotiator.language(Array.from(appState.languages.keys())) ?? backendState.fallback.language; + const availableLanguages = Array.from(appState.languages); + const languageCandidate = negotiator.language(availableLanguages.map((l) => l.name)) ?? backendState.fallback.language; const encodingCandidate = negotiator.encoding(Array.from(appState.encodings.keys())) ?? backendState.fallback.encoding; const contentTypeCandidate = negotiator.mediaType(Array.from(appState.serializers.keys())) ?? backendState.fallback.serializer; - const availableLanguages = Array.from(appState.languages.entries()); - const fallbackMessageCollection = en.messages as MessageCollection; + const fallbackMessageCollection = en as Language; const fallbackSerializerPair = applicationJson as SerializerPair; const fallbackEncoding = utf8 as EncodingPair; const errorLanguageCode = backendState.errorHeaders.language ?? backendState.fallback.language; - const errorMessageCollection = appState.languages.get(errorLanguageCode) ?? fallbackMessageCollection; + const errorMessageCollection = availableLanguages.find((l) => l.name === errorLanguageCode) ?? fallbackMessageCollection; const errorContentType = backendState.errorHeaders.serializer ?? backendState.fallback.serializer; const errorSerializerPair = appState.serializers.get(errorContentType) ?? fallbackSerializerPair; @@ -480,8 +441,8 @@ export const application = (appParams: ApplicationParams): Application => { const errorEncoding = appState.encodings.get(errorEncodingKey) ?? fallbackEncoding; // TODO refactor - const [currentLanguageCode, currentLanguageMessages] = availableLanguages.find(([code]) => code === languageCandidate) ?? []; - if (typeof currentLanguageCode === 'undefined' || typeof currentLanguageMessages === 'undefined') { + const currentLanguageMessages = availableLanguages.find((l) => l.name === languageCandidate); + if (typeof currentLanguageMessages === 'undefined') { const data = errorMessageCollection.bodies.languageNotAcceptable(); const responseRaw = errorSerializerPair.serialize(data); const response = errorEncoding.encode(responseRaw); @@ -538,10 +499,10 @@ export const application = (appParams: ApplicationParams): Application => { query, responseBodyEncoding: [currentEncoding, responseBodyEncodingEntry], responseBodyMediaType: [currentContentTypeMimeType, responseMediaTypeEntry], - responseBodyLanguage: [currentLanguageCode, currentLanguageMessages], + responseBodyLanguage: currentLanguageMessages, errorResponseBodyMediaType: [errorContentType, errorSerializerPair], errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage: errorMessageCollection, }; const methodAndUrl = await handleHasMethodAndUrl(middlewareArgs as MiddlewareArgs)(req, res); diff --git a/src/handlers.ts b/src/handlers.ts index 075033e..2f7ef17 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -5,15 +5,15 @@ import {getBody, getDeserializerObjects} from './utils'; import {IncomingMessage, ServerResponse} from 'http'; export const handleHasMethodAndUrl: Middleware = ({ - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, }) => (req: IncomingMessage, res: ServerResponse) => { if (!req.method) { res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { 'Allow': 'HEAD, GET, POST, PUT, PATCH, DELETE', - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.methodNotAllowed(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.methodNotAllowed(); res.end(); return { handled: true @@ -22,7 +22,7 @@ export const handleHasMethodAndUrl: Middleware = ({ if (!req.url) { res.statusCode = constants.HTTP_STATUS_BAD_REQUEST; - res.statusMessage = errorMessageCollection.statusMessages.badRequest(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.badRequest(); res.end(); return { handled: true @@ -39,9 +39,9 @@ export const handleGetRoot: Middleware = ({ appParams, serverParams, responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], - responseBodyLanguage: [languageCode, responseBodyMessageCollection], + responseBodyLanguage, responseBodyEncoding: [encodingKey, encoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, }) => (_req: IncomingMessage, res: ServerResponse) => { const data = { name: appParams.name @@ -52,9 +52,9 @@ export const handleGetRoot: Middleware = ({ serialized = responseBodySerializerPair.serialize(data); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToSerializeResponse(); res.end(); return { handled: true, @@ -66,9 +66,9 @@ export const handleGetRoot: Middleware = ({ encoded = encoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEncodeResponse(); res.end(); return { handled: true, @@ -77,7 +77,7 @@ export const handleGetRoot: Middleware = ({ const theHeaders: Record = { 'Content-Type': responseBodyMediaType, - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, 'Content-Encoding': encodingKey, }; @@ -91,14 +91,14 @@ export const handleGetRoot: Middleware = ({ // we are using custom headers for links because the standard Link header // is referring to the document metadata (e.g. author, next page, etc) // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link - theHeaders['X-Resource-Link'] = availableResources + theHeaders['Link'] = availableResources .map((r) => - `<${serverParams.baseUrl}/${r.state.routeName}>; name="${r.state.collectionName}"`, + `<${serverParams.baseUrl}/${r.state.routeName}>; rel="related"; name="${r.state.collectionName}"`, ) .join(', '); } res.writeHead(constants.HTTP_STATUS_OK, theHeaders); - res.statusMessage = responseBodyMessageCollection.statusMessages.ok(); + res.statusMessage = responseBodyLanguage.statusMessages.ok(); res.end(encoded); return { handled: true @@ -108,9 +108,9 @@ export const handleGetRoot: Middleware = ({ export const handleGetCollection: Middleware = ({ resource, responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], - responseBodyLanguage: [languageCode, responseBodyMessageCollection], + responseBodyLanguage, responseBodyEncoding: [encodingKey, encoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, backendState, query, }) => async (_req: IncomingMessage, res: ServerResponse) => { @@ -118,9 +118,9 @@ export const handleGetCollection: Middleware = ({ await resource.dataSource.initialize(); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToInitializeResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToInitializeResourceDataSource(resource); res.end(); return { handled: true @@ -137,9 +137,9 @@ export const handleGetCollection: Middleware = ({ } } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToFetchResourceCollection(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToFetchResourceCollection(resource); res.end(); return { handled: true @@ -151,9 +151,9 @@ export const handleGetCollection: Middleware = ({ serialized = responseBodySerializerPair.serialize(data); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToSerializeResponse(); res.end(); return { handled: true, @@ -165,9 +165,9 @@ export const handleGetCollection: Middleware = ({ encoded = encoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEncodeResponse(); res.end(); return { handled: true, @@ -176,7 +176,7 @@ export const handleGetCollection: Middleware = ({ const headers: Record = { 'Content-Type': responseBodyMediaType, - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, 'Content-Encoding': encodingKey, }; @@ -185,7 +185,7 @@ export const handleGetCollection: Middleware = ({ } res.writeHead(constants.HTTP_STATUS_OK, headers); - res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCollectionFetched(resource); + res.statusMessage = responseBodyLanguage.statusMessages.resourceCollectionFetched(resource); res.end(encoded); return { handled: true @@ -196,17 +196,17 @@ export const handleGetItem: Middleware = ({ resourceId, resource, responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], - responseBodyLanguage: [languageCode, responseBodyMessageCollection], + responseBodyLanguage, responseBodyEncoding: [encodingKey, encoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, }) => async (_req: IncomingMessage, res: ServerResponse) => { try { await resource.dataSource.initialize(); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToInitializeResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToInitializeResourceDataSource(resource); res.end(); return { handled: true @@ -218,9 +218,9 @@ export const handleGetItem: Middleware = ({ data = await resource.dataSource.getById(resourceId); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToFetchResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToFetchResource(resource); res.end(); return { handled: true @@ -232,9 +232,9 @@ export const handleGetItem: Middleware = ({ serialized = data === null ? null : responseBodySerializerPair.serialize(data); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToSerializeResponse(); res.end(); return { handled: true, @@ -246,9 +246,9 @@ export const handleGetItem: Middleware = ({ encoded = serialized === null ? null : encoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEncodeResponse(); res.end(); return { handled: true, @@ -258,10 +258,10 @@ export const handleGetItem: Middleware = ({ if (encoded) { res.writeHead(constants.HTTP_STATUS_OK, { 'Content-Type': responseBodyMediaType, - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, 'Content-Encoding': encodingKey, }); - res.statusMessage = responseBodyMessageCollection.statusMessages.resourceFetched(resource) + res.statusMessage = responseBodyLanguage.statusMessages.resourceFetched(resource) res.end(encoded); return { handled: true @@ -269,9 +269,9 @@ export const handleGetItem: Middleware = ({ } res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.resourceNotFound(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.resourceNotFound(resource); res.end(); return { handled: true @@ -282,17 +282,17 @@ export const handleGetItem: Middleware = ({ export const handleDeleteItem: Middleware = ({ resource, resourceId, - responseBodyLanguage: [languageCode, responseBodyMessageCollection], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + responseBodyLanguage, + errorResponseBodyLanguage, backendState, }) => async (_req: IncomingMessage, res: ServerResponse) => { try { await resource.dataSource.initialize(); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToInitializeResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToInitializeResourceDataSource(resource); res.end(); return { handled: true @@ -304,9 +304,9 @@ export const handleDeleteItem: Middleware = ({ existing = await resource.dataSource.getById(resourceId); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToFetchResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToFetchResource(resource); res.end(); return { handled: true @@ -315,9 +315,9 @@ export const handleDeleteItem: Middleware = ({ if (!existing && backendState.throws404OnDeletingNotFound) { res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.deleteNonExistingResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.deleteNonExistingResource(resource); res.end(); return { handled: true @@ -330,9 +330,9 @@ export const handleDeleteItem: Middleware = ({ } } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToDeleteResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToDeleteResource(resource); res.end(); return { handled: true @@ -340,9 +340,9 @@ export const handleDeleteItem: Middleware = ({ } res.writeHead(constants.HTTP_STATUS_NO_CONTENT, { - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, }); - res.statusMessage = responseBodyMessageCollection.statusMessages.resourceDeleted(resource); + res.statusMessage = responseBodyLanguage.statusMessages.resourceDeleted(resource); res.end(); return { handled: true @@ -354,18 +354,18 @@ export const handlePatchItem: Middleware = ({ resource, resourceId, responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], - responseBodyLanguage: [languageCode, responseBodyMessageCollection], + responseBodyLanguage, responseBodyEncoding: [encodingKey, encoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, errorResponseBodyMediaType: [errorMediaType, errorSerializerPair], errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], }) => async (req: IncomingMessage, res: ServerResponse) => { const { deserializerPair: requestBodyDeserializerPair, encodingPair: requestBodyEncodingPair } = getDeserializerObjects(appState, req); if (typeof requestBodyDeserializerPair === 'undefined' || typeof requestBodyEncodingPair === 'undefined') { res.writeHead(constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToDeserializeRequest(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToDeserializeRequest(); res.end(); return { handled: true @@ -376,9 +376,9 @@ export const handlePatchItem: Middleware = ({ await resource.dataSource.initialize(); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToInitializeResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToInitializeResourceDataSource(resource); res.end(); return { handled: true @@ -390,9 +390,9 @@ export const handlePatchItem: Middleware = ({ existing = await resource.dataSource.getById(resourceId); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToFetchResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToFetchResource(resource); res.end(); return { handled: true @@ -401,9 +401,9 @@ export const handlePatchItem: Middleware = ({ if (!existing) { res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.patchNonExistingResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.patchNonExistingResource(resource); res.end(); return { handled: true @@ -428,11 +428,11 @@ export const handlePatchItem: Middleware = ({ } catch (errRaw) { const err = errRaw as v.ValiError; const headers: Record = { - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, }; if (!Array.isArray(err.issues)) { res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, headers) - res.statusMessage = errorMessageCollection.statusMessages.invalidResourcePatch(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResourcePatch(resource); res.end(); return { handled: true, @@ -451,7 +451,7 @@ export const handlePatchItem: Middleware = ({ 'Content-Type': errorMediaType, 'Content-Encoding': errorEncodingKey, }) - res.statusMessage = errorMessageCollection.statusMessages.invalidResourcePatch(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResourcePatch(resource); res.end(encoded); return { handled: true, @@ -464,9 +464,9 @@ export const handlePatchItem: Middleware = ({ newObject = await resource.dataSource.patch(resourceId, params); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToPatchResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToPatchResource(resource); res.end(); return { handled: true, @@ -478,9 +478,9 @@ export const handlePatchItem: Middleware = ({ serialized = responseBodySerializerPair.serialize(newObject); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToSerializeResponse(); res.end(); return { handled: true, @@ -492,9 +492,9 @@ export const handlePatchItem: Middleware = ({ encoded = encoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEncodeResponse(); res.end(); return { handled: true, @@ -503,10 +503,10 @@ export const handlePatchItem: Middleware = ({ res.writeHead(constants.HTTP_STATUS_OK, { 'Content-Type': responseBodyMediaType, - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, 'Content-Encoding': encodingKey, }); - res.statusMessage = responseBodyMessageCollection.statusMessages.resourcePatched(resource); + res.statusMessage = responseBodyLanguage.statusMessages.resourcePatched(resource); res.end(encoded); return { handled: true @@ -520,9 +520,9 @@ export const handleCreateItem: Middleware = ({ serverParams, backendState, responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], - responseBodyLanguage: [languageCode, responseBodyMessageCollection], + responseBodyLanguage, responseBodyEncoding: [encodingKey, encoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, errorResponseBodyMediaType: [errorMediaType, errorSerializerPair], errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], resource, @@ -530,9 +530,9 @@ export const handleCreateItem: Middleware = ({ const { deserializerPair: requestBodyDeserializerPair, encodingPair: requestBodyEncodingPair } = getDeserializerObjects(appState, req); if (typeof requestBodyDeserializerPair === 'undefined' || typeof requestBodyEncodingPair === 'undefined') { res.writeHead(constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToDeserializeRequest(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToDeserializeRequest(); res.end(); return { handled: true @@ -550,11 +550,11 @@ export const handleCreateItem: Middleware = ({ } catch (errRaw) { const err = errRaw as v.ValiError; const headers: Record = { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }; if (!Array.isArray(err.issues)) { res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, headers) - res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResource(resource); res.end(); return { handled: true, @@ -573,7 +573,7 @@ export const handleCreateItem: Middleware = ({ 'Content-Type': errorMediaType, 'Content-Encoding': errorEncodingKey, }) - res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResource(resource); res.end(encoded); return { handled: true, @@ -584,9 +584,9 @@ export const handleCreateItem: Middleware = ({ await resource.dataSource.initialize(); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToInitializeResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToInitializeResourceDataSource(resource); res.end(); return { handled: true @@ -603,9 +603,9 @@ export const handleCreateItem: Middleware = ({ params[resource.state.idAttr] = newId; } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToGenerateIdFromResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToGenerateIdFromResourceDataSource(resource); res.end(); return { handled: true, @@ -631,9 +631,9 @@ export const handleCreateItem: Middleware = ({ serialized = responseBodySerializerPair.serialize(newObject); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToSerializeResponse(); res.end(); return { handled: true, @@ -645,9 +645,9 @@ export const handleCreateItem: Middleware = ({ encoded = encoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEncodeResponse(); res.end(); return { handled: true, @@ -656,7 +656,7 @@ export const handleCreateItem: Middleware = ({ const headers: Record = { 'Content-Type': responseBodyMediaType, - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, 'Content-Encoding': encodingKey, 'Location': `${serverParams.baseUrl}/${resource.state.routeName}/${newId}` }; @@ -664,7 +664,7 @@ export const handleCreateItem: Middleware = ({ headers['X-Resource-Total-Item-Count'] = totalItemCount.toString(); } res.writeHead(constants.HTTP_STATUS_CREATED, headers); - res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCreated(resource); + res.statusMessage = responseBodyLanguage.statusMessages.resourceCreated(resource); res.end(encoded); return { handled: true @@ -675,9 +675,9 @@ export const handleEmplaceItem: Middleware = ({ appState, serverParams, responseBodyMediaType: [responseBodyMediaType, responseBodySerializerPair], - responseBodyLanguage: [languageCode, responseBodyMessageCollection], + responseBodyLanguage, responseBodyEncoding: [encodingKey, encoding], - errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], + errorResponseBodyLanguage, errorResponseBodyMediaType: [errorMediaType, errorSerializerPair], errorResponseBodyEncoding: [errorEncodingKey, errorEncoding], resource, @@ -687,9 +687,9 @@ export const handleEmplaceItem: Middleware = ({ const { deserializerPair: requestBodyDeserializerPair, encodingPair: requestBodyEncodingPair } = getDeserializerObjects(appState, req); if (typeof requestBodyDeserializerPair === 'undefined' || typeof requestBodyEncodingPair === 'undefined') { res.writeHead(constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToDeserializeRequest(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToDeserializeRequest(); res.end(); return { handled: true @@ -719,11 +719,11 @@ export const handleEmplaceItem: Middleware = ({ } catch (errRaw) { const err = errRaw as v.ValiError; const headers: Record = { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }; if (!Array.isArray(err.issues)) { res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, headers) - res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResource(resource); res.end(); return { handled: true, @@ -742,7 +742,7 @@ export const handleEmplaceItem: Middleware = ({ 'Content-Type': errorMediaType, 'Content-Encoding': errorEncodingKey, }) - res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.invalidResource(resource); res.end(encoded); return { handled: true, @@ -753,9 +753,9 @@ export const handleEmplaceItem: Middleware = ({ await resource.dataSource.initialize(); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToInitializeResourceDataSource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToInitializeResourceDataSource(resource); res.end(); return { handled: true @@ -770,9 +770,9 @@ export const handleEmplaceItem: Middleware = ({ [newObject, isCreated] = await resource.dataSource.emplace(resourceId, params); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEmplaceResource(resource); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEmplaceResource(resource); res.end(); return { handled: true @@ -784,9 +784,9 @@ export const handleEmplaceItem: Middleware = ({ serialized = responseBodySerializerPair.serialize(newObject); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToSerializeResponse(); res.end(); return { handled: true, @@ -798,9 +798,9 @@ export const handleEmplaceItem: Middleware = ({ encoded = encoding.encode(serialized); } catch { res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { - 'Content-Language': errorLanguageCode, + 'Content-Language': errorResponseBodyLanguage.name, }); - res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); + res.statusMessage = errorResponseBodyLanguage.statusMessages.unableToEncodeResponse(); res.end(); return { handled: true, @@ -809,7 +809,7 @@ export const handleEmplaceItem: Middleware = ({ const headers: Record = { 'Content-Type': responseBodyMediaType, - 'Content-Language': languageCode, + 'Content-Language': responseBodyLanguage.name, 'Content-Encoding': encodingKey, }; let totalItemCount: number | undefined; @@ -825,8 +825,8 @@ export const handleEmplaceItem: Middleware = ({ res.writeHead(isCreated ? constants.HTTP_STATUS_CREATED : constants.HTTP_STATUS_OK, headers); res.statusMessage = ( isCreated - ? responseBodyMessageCollection.statusMessages.resourceCreated(resource) - : responseBodyMessageCollection.statusMessages.resourceReplaced(resource) + ? responseBodyLanguage.statusMessages.resourceCreated(resource) + : responseBodyLanguage.statusMessages.resourceReplaced(resource) ); res.end(encoded); return { diff --git a/src/languages/en/index.ts b/src/languages/en/index.ts index eb69b2f..365e7ab 100644 --- a/src/languages/en/index.ts +++ b/src/languages/en/index.ts @@ -1,103 +1,103 @@ -import {MessageCollection, Resource} from '../../core'; +import {Resource} from '../../core'; +import {Language} from '../../common'; -export const messages: MessageCollection = { - statusMessages: { - unableToSerializeResponse(): string { - return 'Unable To Serialize Response'; - }, - unableToEncodeResponse(): string { - return 'Unable To Encode Response'; - }, - unableToInitializeResourceDataSource(resource: Resource): string { - return `Unable To Initialize ${resource.state.itemName} Data Source`; - }, - unableToFetchResourceCollection(resource: Resource): string { - return `Unable To Fetch ${resource.state.itemName} Collection`; - }, - unableToFetchResource(resource: Resource): string { - return `Unable To Fetch ${resource.state.itemName}`; - }, - unableToDeleteResource(resource: Resource): string { - return `Unable To Delete ${resource.state.itemName}`; - }, - languageNotAcceptable(): string { - return 'Language Not Acceptable'; - }, - encodingNotAcceptable(): string { - return 'Encoding Not Acceptable'; - }, - mediaTypeNotAcceptable(): string { - return 'Media Type Not Acceptable'; - }, - methodNotAllowed(): string { - return 'Method Not Allowed'; - }, - urlNotFound(): string { - return 'URL Not Found'; - }, - badRequest(): string { - return 'Bad Request'; - }, - ok(): string { - return 'OK'; - }, - resourceCollectionFetched(resource: Resource): string { - return `${resource.state.itemName} Collection Fetched`; - }, - resourceFetched(resource: Resource): string { - return `${resource.state.itemName} Fetched`; - }, - resourceNotFound(resource: Resource): string { - return `${resource.state.itemName} Not Found`; - }, - deleteNonExistingResource(resource: Resource): string { - return `Delete Non-Existing ${resource.state.itemName}`; - }, - resourceDeleted(resource: Resource): string { - return `${resource.state.itemName} Deleted`; - }, - unableToDeserializeRequest(): string { - return 'Unable To Deserialize Request'; - }, - patchNonExistingResource(resource: Resource): string { - return `Patch Non-Existing ${resource.state.itemName}`; - }, - unableToPatchResource(resource: Resource): string { - return `Unable To Patch ${resource.state.itemName}`; - }, - invalidResourcePatch(resource: Resource): string { - return `Invalid ${resource.state.itemName} Patch`; - }, - invalidResource(resource: Resource): string { - return `Invalid ${resource.state.itemName}`; - }, - resourcePatched(resource: Resource): string { - return `${resource.state.itemName} Patched`; - }, - resourceCreated(resource: Resource): string { - return `${resource.state.itemName} Created`; - }, - resourceReplaced(resource: Resource): string { - return `${resource.state.itemName} Replaced`; - }, - unableToGenerateIdFromResourceDataSource(resource: Resource): string { - return `Unable To Generate ID From ${resource.state.itemName} Data Source`; - }, - unableToEmplaceResource(resource: Resource): string { - return `Unable To Emplace ${resource.state.itemName}`; - } - }, - bodies: { - languageNotAcceptable() { - return []; - }, - encodingNotAcceptable() { - return []; - }, - mediaTypeNotAcceptable() { - return [] - } +export const statusMessages = { + unableToSerializeResponse(): string { + return 'Unable To Serialize Response'; + }, + unableToEncodeResponse(): string { + return 'Unable To Encode Response'; + }, + unableToInitializeResourceDataSource(resource: Resource): string { + return `Unable To Initialize ${resource.state.itemName} Data Source`; + }, + unableToFetchResourceCollection(resource: Resource): string { + return `Unable To Fetch ${resource.state.itemName} Collection`; + }, + unableToFetchResource(resource: Resource): string { + return `Unable To Fetch ${resource.state.itemName}`; + }, + unableToDeleteResource(resource: Resource): string { + return `Unable To Delete ${resource.state.itemName}`; + }, + languageNotAcceptable(): string { + return 'Language Not Acceptable'; + }, + encodingNotAcceptable(): string { + return 'Encoding Not Acceptable'; + }, + mediaTypeNotAcceptable(): string { + return 'Media Type Not Acceptable'; + }, + methodNotAllowed(): string { + return 'Method Not Allowed'; + }, + urlNotFound(): string { + return 'URL Not Found'; + }, + badRequest(): string { + return 'Bad Request'; + }, + ok(): string { + return 'OK'; + }, + resourceCollectionFetched(resource: Resource): string { + return `${resource.state.itemName} Collection Fetched`; + }, + resourceFetched(resource: Resource): string { + return `${resource.state.itemName} Fetched`; + }, + resourceNotFound(resource: Resource): string { + return `${resource.state.itemName} Not Found`; + }, + deleteNonExistingResource(resource: Resource): string { + return `Delete Non-Existing ${resource.state.itemName}`; + }, + resourceDeleted(resource: Resource): string { + return `${resource.state.itemName} Deleted`; + }, + unableToDeserializeRequest(): string { + return 'Unable To Deserialize Request'; + }, + patchNonExistingResource(resource: Resource): string { + return `Patch Non-Existing ${resource.state.itemName}`; + }, + unableToPatchResource(resource: Resource): string { + return `Unable To Patch ${resource.state.itemName}`; + }, + invalidResourcePatch(resource: Resource): string { + return `Invalid ${resource.state.itemName} Patch`; + }, + invalidResource(resource: Resource): string { + return `Invalid ${resource.state.itemName}`; + }, + resourcePatched(resource: Resource): string { + return `${resource.state.itemName} Patched`; + }, + resourceCreated(resource: Resource): string { + return `${resource.state.itemName} Created`; + }, + resourceReplaced(resource: Resource): string { + return `${resource.state.itemName} Replaced`; + }, + unableToGenerateIdFromResourceDataSource(resource: Resource): string { + return `Unable To Generate ID From ${resource.state.itemName} Data Source`; + }, + unableToEmplaceResource(resource: Resource): string { + return `Unable To Emplace ${resource.state.itemName}`; + } +} satisfies Language['statusMessages']; + +export const bodies = { + languageNotAcceptable() { + return []; + }, + encodingNotAcceptable() { + return []; + }, + mediaTypeNotAcceptable() { + return [] } -}; +} satisfies Language['bodies']; -export const name = 'en'; +export const name = 'en' as const;