Skip to content

Commit

Permalink
Merge pull request #391 from AliMD/feat/storage
Browse files Browse the repository at this point in the history
refactor(service/storage): rewrite structure
  • Loading branch information
alimd authored Nov 15, 2022
2 parents e8c4627 + ad5588c commit 61b33fb
Show file tree
Hide file tree
Showing 21 changed files with 405 additions and 263 deletions.
42 changes: 37 additions & 5 deletions packages/core/nano-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,25 @@ connection.reply({

Request URL.

### `connection.method: "\*" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "TRACE" | "OPTIONS" | "PATCH"
### `connection.method: "ALL" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "TRACE" | "OPTIONS" | "PATCH"

`

Request method.

### `connection.token: string | null`
### `connection.getBody(): Promise<string | null>`

The token placed in the request header.
Get request body for **POST**, **PUT** and **POST** methods..

### `connection.bodyPromise: string | null`
Example:

```ts
const body = await connection.getBody();
```

### `connection.getToken(): string | null`

Request body for **POST** & **PUT** method.
Get the token placed in the request header.

### `connection.reply(content: ReplyContent)`

Expand All @@ -110,4 +116,30 @@ Example:

```ts
const bodyData = await connection.requireJsonBody();
if (bodyData == null) return;
```

### `requireToken(validator: ((token: string) => boolean) | Array<string> | string): string | null`

Parse and validate request token.
Returns request token.

Example:

```ts
const token = connection.requireToken((token) => token.length > 12);
if (token == null) return;
```

### `requireQueryParams<T>(params: Record<string, ParamType>): T | null`

Parse and validate query params.
Returns query params object.

Example:

```ts
const params = connection.requireQueryParams<{id: string}>({id: 'string'});
if (params == null) return;
console.log(params.id);
```
1 change: 1 addition & 0 deletions packages/core/nano-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"@alwatr/logger": "^0.21.0",
"@alwatr/math": "^0.21.0",
"tslib": "^2.4.1"
}
}
178 changes: 152 additions & 26 deletions packages/core/nano-server/src/nano-server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {createServer} from 'http';

import {alwatrRegisteredList, createLogger} from '@alwatr/logger';
import {isNumber} from '@alwatr/math';

import type {Config, Methods, ReplyContent} from './type.js';
import type {Config, Methods, ParamType, QueryParams, ReplyContent} from './type.js';
import type {AlwatrLogger} from '@alwatr/logger';
import type {IncomingMessage, ServerResponse} from 'node:http';
import type {Duplex} from 'node:stream';
Expand Down Expand Up @@ -102,7 +103,11 @@ export class AlwatrNanoServer {
* });
* ```
*/
route(method: Methods, route: 'all' | `/${string}`, middleware: (connection: AlwatrConnection) => void): void {
route(
method: 'ALL' | Methods,
route: 'all' | `/${string}`,
middleware: (connection: AlwatrConnection) => void,
): void {
this._logger.logMethodArgs('route', {method, route});

if (this.middlewareList[method] == null) this.middlewareList[method] = {};
Expand Down Expand Up @@ -154,7 +159,7 @@ export class AlwatrNanoServer {

// prettier-ignore
protected middlewareList: Record<string, Record<string, (connection: AlwatrConnection) => void | Promise<void>>> = {
all: {},
ALL: {},
};

protected async _requestListener(incomingMessage: IncomingMessage, serverResponse: ServerResponse): Promise<void> {
Expand All @@ -177,9 +182,9 @@ export class AlwatrNanoServer {

const middleware =
this.middlewareList[connection.method]?.[route] ||
this.middlewareList.all[route] ||
this.middlewareList.ALL[route] ||
this.middlewareList[connection.method]?.all ||
this.middlewareList.all.all;
this.middlewareList.ALL.all;

try {
if (typeof middleware === 'function') {
Expand Down Expand Up @@ -225,26 +230,14 @@ export class AlwatrConnection {
* Request URL.
*/
readonly url = new URL(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.incomingMessage.url!.replace(AlwatrConnection.versionPattern, ''),
'http://localhost/',
(this.incomingMessage.url ?? '').replace(AlwatrConnection.versionPattern, ''),
'http://localhost/',
);

/**
* Request method.
*/
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
readonly method = this.incomingMessage.method!.toUpperCase() as Methods;

/**
* The token placed in the request header.
*/
readonly token = this._getToken();

/**
* Request body for POST & PUT method.
*/
readonly bodyPromise = this._getRequestBody();
readonly method = (this.incomingMessage.method ?? 'GET').toUpperCase() as Methods;

protected _logger = createLogger(`alwatr-nano-server-connection`);

Expand Down Expand Up @@ -314,7 +307,10 @@ export class AlwatrConnection {
this.serverResponse.end();
}

protected _getToken(): string | null {
/**
* Get the token placed in the request header.
*/
getToken(): string | null {
const auth = this.incomingMessage.headers.authorization?.split(' ');

if (auth == null || auth[0] !== 'Bearer') {
Expand All @@ -324,9 +320,17 @@ export class AlwatrConnection {
return auth[1];
}

protected async _getRequestBody(): Promise<string | null> {
/**
* Get request body for POST, PUT and POST methods.
*
* Example:
* ```ts
* const body = await connection.getBody();
* ```
*/
async getBody(): Promise<string | null> {
// method must be POST or PUT
if (!(this.method === 'POST' || this.method === 'PUT')) {
if (!(this.method === 'POST' || this.method === 'PUT' || this.method === 'PATCH')) {
return null;
}

Expand All @@ -349,13 +353,23 @@ export class AlwatrConnection {
* Example:
* ```ts
* const bodyData = await connection.requireJsonBody();
* if (bodyData == null) return;
* ```
*/
async requireJsonBody<Type extends Record<string, unknown>>(): Promise<Type | null> {
// if request content type is json, parse the body
const body = await this.bodyPromise;
// if request content type is json
if (this.incomingMessage.headers['content-type'] !== 'application/json') {
this.reply({
ok: false,
statusCode: 400,
errorCode: 'require_body_json',
});
return null;
}

const body = await this.getBody();

if (body === null || body.length === 0) {
if (body == null || body.length === 0) {
this.reply({
ok: false,
statusCode: 400,
Expand All @@ -376,4 +390,116 @@ export class AlwatrConnection {
return null;
}
}

/**
* Parse and validate request token.
*
* @returns Request token.
*
* Example:
* ```ts
* const token = connection.requireToken((token) => token.length > 12);
* if (token == null) return;
* ```
*/
requireToken(validator?: ((token: string) => boolean) | Array<string> | string): string | null {
const token = this.getToken();

if (token == null) {
this.reply({
ok: false,
statusCode: 401,
errorCode: 'authorization_required',
});
return null;
}
else if (validator === undefined) {
return token;
}
else if (typeof validator === 'string') {
if (token === validator) return token;
}
else if (Array.isArray(validator)) {
if (validator.includes(token)) return token;
}
else if (typeof validator === 'function') {
if (validator(token) === true) return token;
}
this.reply({
ok: false,
statusCode: 403,
errorCode: 'access_denied',
});
return null;
}

/**
* Parse query param and validate with param type
*/
protected _sanitizeParam(name: string, type: ParamType): string | number | boolean | null {
let value: string | number | boolean | null = this.url.searchParams.get(name);

if (value == null || value.length === 0) {
return null;
}

if (type === 'string') {
return value;
}

value = value.trim();

if (type === 'number') {
return isNumber(value) ? +value : null;
}

if (type === 'boolean') {
if (value === 'true' || value === '1') {
value = true;
}
else if (value === 'false' || value === '0') {
value = false;
}
else return null;
}

return null;
}

/**
* Parse and validate query params.
*
* @returns Query params object.
*
* Example:
* ```ts
* const params = connection.requireQueryParams<{id: string}>({id: 'string'});
* if (params == null) return;
* console.log(params.id);
* ```
*/
requireQueryParams<T extends QueryParams = QueryParams>(params: Record<string, ParamType>): T | null {
const parsedParams: Record<string, string | number | boolean | null> = {};

for (const paramName in params) {
if (!Object.prototype.hasOwnProperty.call(params, paramName)) continue;
const paramType = params[paramName];
const paramValue = (parsedParams[paramName] = this._sanitizeParam(paramName, paramType));
if (paramValue == null) {
this.reply({
ok: false,
statusCode: 406,
errorCode: `query_parameter_required`,
data: {
paramName,
paramType,
paramValue,
},
});
return null;
}
}

return parsedParams as T;
}
}
5 changes: 4 additions & 1 deletion packages/core/nano-server/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface ReplySuccessContent {

export type ReplyContent = ReplyFailedContent | ReplySuccessContent;

export type Methods = '*' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'TRACE' | 'OPTIONS' | 'PATCH';
export type Methods = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'TRACE' | 'OPTIONS' | 'PATCH';

export interface Config {
/**
Expand All @@ -34,3 +34,6 @@ export interface Config {
*/
autoListen: boolean;
}

export type QueryParams = Record<string, string | number | boolean>;
export type ParamType = 'string' | 'number' | 'boolean';
6 changes: 3 additions & 3 deletions packages/core/storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,14 @@ Example:
if (!useruserStorage.has('user-1')) throw new Error('user not found');
```

### `remove(documentId: string): boolean`
### `delete(documentId: string): boolean`

Remove a document object from the storage.
Delete a document object from the storage.

Example:

```ts
userStorage.remove('user-1');
userStorage.delete('user-1');
```

### `async forAll(callbackfn: (documentObject: DocumentType) => void | false | Promise<void | false>): Promise<void>`
Expand Down
8 changes: 4 additions & 4 deletions packages/core/storage/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,16 @@ export class AlwatrStorage<DocumentType extends DocumentObject> {
}

/**
* Remove a document object from the storage.
* Delete a document object from the storage.
*
* Example:
*
* ```ts
* userStorage.remove('user-1');
* userStorage.delete('user-1');
* ```
*/
remove(documentId: string): boolean {
this._logger.logMethodArgs('remove', documentId);
delete(documentId: string): boolean {
this._logger.logMethodArgs('delete', documentId);

if (this._storage.data[documentId] == null) {
return false;
Expand Down
10 changes: 8 additions & 2 deletions packages/nanoservice/storage/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Alwatr Storage Nanoservice API
# Alwatr Storage Server

Elegant micro in-memory json-like storage with disk backed, Fastest NoSQL Database.
Elegant micro in-memory json-like storage nanoservice with disk backed, Fastest NoSQL Database.

## How to use

Use [`@alwatr/storage-client`](../../core/storage-client/) in your API.

Check [demo.http](demo.http) to manual request (not recommended)!
Loading

0 comments on commit 61b33fb

Please sign in to comment.