Browse Source

Refactor codebase

Organize code into certain file structure.
master
TheoryOfNekomata 8 months ago
parent
commit
b36167026d
6 changed files with 327 additions and 273 deletions
  1. +34
    -0
      README.md
  2. +15
    -0
      TODO.md
  3. +44
    -0
      src/common.ts
  4. +26
    -65
      src/core.ts
  5. +108
    -108
      src/handlers.ts
  6. +100
    -100
      src/languages/en/index.ts

+ 34
- 0
README.md View File

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

+ 15
- 0
TODO.md View File

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

+ 44
- 0
src/common.ts View File

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

+ 26
- 65
src/core.ts View File

@@ -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<ResourceSchema, IdAttr, IdSchema>;
};

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<ResourceWithDataSource<any>>;
languages: Map<string, MessageCollection>;
languages: Set<Language>;
serializers: Map<string, SerializerPair>;
encodings: Map<string, EncodingPair>;
}

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<T extends v.BaseSchema> {
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<T>;
@@ -328,6 +281,14 @@ export interface Middleware {
<T extends v.BaseSchema = v.BaseSchema>(args: MiddlewareArgs<T>): RequestListenerWithReturn<HandlerState | Promise<HandlerState>>
}

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<T extends v.BaseSchema>(resRaw: Partial<Resource<T>>): this;
createBackend(): Backend;
@@ -354,12 +315,12 @@ export interface Application {
export const application = (appParams: ApplicationParams): Application => {
const appState: ApplicationState = {
resources: new Set<ResourceWithDataSource<any>>(),
languages: new Map<string, MessageCollection>(),
languages: new Set<Language>(),
serializers: new Map<string, SerializerPair>(),
encodings: new Map<string, EncodingPair>(),
};

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<T extends v.BaseSchema>(resRaw: Partial<Resource<T>>) {
@@ -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<never>)(req, res);


+ 108
- 108
src/handlers.ts View File

@@ -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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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 {


+ 100
- 100
src/languages/en/index.ts View File

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

Loading…
Cancel
Save