diff --git a/packages/core/nano-server/README.md b/packages/core/nano-server/README.md index 34d6d7682..3b51241c2 100644 --- a/packages/core/nano-server/README.md +++ b/packages/core/nano-server/README.md @@ -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` -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)` @@ -110,4 +116,30 @@ Example: ```ts const bodyData = await connection.requireJsonBody(); +if (bodyData == null) return; +``` + +### `requireToken(validator: ((token: string) => boolean) | Array | 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(params: Record): 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); ``` diff --git a/packages/core/nano-server/package.json b/packages/core/nano-server/package.json index 59512a2d5..2f81b77cc 100644 --- a/packages/core/nano-server/package.json +++ b/packages/core/nano-server/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@alwatr/logger": "^0.21.0", + "@alwatr/math": "^0.21.0", "tslib": "^2.4.1" } } diff --git a/packages/core/nano-server/src/nano-server.ts b/packages/core/nano-server/src/nano-server.ts index 6fcebbaac..015c75671 100644 --- a/packages/core/nano-server/src/nano-server.ts +++ b/packages/core/nano-server/src/nano-server.ts @@ -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'; @@ -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] = {}; @@ -154,7 +159,7 @@ export class AlwatrNanoServer { // prettier-ignore protected middlewareList: Record void | Promise>> = { - all: {}, + ALL: {}, }; protected async _requestListener(incomingMessage: IncomingMessage, serverResponse: ServerResponse): Promise { @@ -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') { @@ -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`); @@ -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') { @@ -324,9 +320,17 @@ export class AlwatrConnection { return auth[1]; } - protected async _getRequestBody(): Promise { + /** + * Get request body for POST, PUT and POST methods. + * + * Example: + * ```ts + * const body = await connection.getBody(); + * ``` + */ + async getBody(): Promise { // 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; } @@ -349,13 +353,23 @@ export class AlwatrConnection { * Example: * ```ts * const bodyData = await connection.requireJsonBody(); + * if (bodyData == null) return; * ``` */ async requireJsonBody>(): Promise { - // 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, @@ -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 | 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(params: Record): T | null { + const parsedParams: Record = {}; + + 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; + } } diff --git a/packages/core/nano-server/src/type.ts b/packages/core/nano-server/src/type.ts index deed6505b..32b18fbb5 100644 --- a/packages/core/nano-server/src/type.ts +++ b/packages/core/nano-server/src/type.ts @@ -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 { /** @@ -34,3 +34,6 @@ export interface Config { */ autoListen: boolean; } + +export type QueryParams = Record; +export type ParamType = 'string' | 'number' | 'boolean'; diff --git a/packages/core/storage/README.md b/packages/core/storage/README.md index 1894a6876..a48cd97d5 100644 --- a/packages/core/storage/README.md +++ b/packages/core/storage/README.md @@ -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): Promise` diff --git a/packages/core/storage/src/storage.ts b/packages/core/storage/src/storage.ts index 77959d808..a133b0e1a 100644 --- a/packages/core/storage/src/storage.ts +++ b/packages/core/storage/src/storage.ts @@ -246,16 +246,16 @@ export class AlwatrStorage { } /** - * 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; diff --git a/packages/nanoservice/storage/README.md b/packages/nanoservice/storage/README.md index 81ddbadb5..17de233cd 100644 --- a/packages/nanoservice/storage/README.md +++ b/packages/nanoservice/storage/README.md @@ -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)! diff --git a/packages/nanoservice/storage/demo.http b/packages/nanoservice/storage/demo.http index 611b6c55a..fc5fda923 100644 --- a/packages/nanoservice/storage/demo.http +++ b/packages/nanoservice/storage/demo.http @@ -1,74 +1,90 @@ @apiUrl = http://localhost:80 @apiVersion = v0 -@token = demo_123456789_123456789_123456789_123456789_123456789 +@token = alwatr_110_313 -### Get a public storage -GET {{apiUrl}}/{{apiVersion}}/comment-list/page-1.json +### Get a public storage over nginx serve +# GET {{apiUrl}}/{{apiVersion}}/public/comments/page1.json -### Get a document by storageName/docId -GET {{apiUrl}}/{{apiVersion}}/sample/doc-id-1 +### Get a document by storageName and docId +GET {{apiUrl}}/{{apiVersion}}/?storage=comments/page1&id=c1 authorization: Bearer {{token}} -### Update/insert a document in storageName -POST {{apiUrl}}/{{apiVersion}}/sample +### Insert document +PATCH {{apiUrl}}/{{apiVersion}}/?storage=comments/page1 authorization: Bearer {{token}} Content-Type: application/json { - "_id": "doc-id-1", + "_id": "c1", + "_updatedBy": "demo", "from": "Ali Mihandoost", - "message": "Salam ;)" + "message": "Salam" } -### Insert a document to storageName -PUT {{apiUrl}}/{{apiVersion}}/sample +### +PATCH {{apiUrl}}/{{apiVersion}}/?storage=comments/page1 authorization: Bearer {{token}} Content-Type: application/json { - "from": "Ali Mihandoost", - "message": "Salam ;)" + "_id": "c2", + "_updatedBy": "demo", + "from": "Fateme Mihandoost", + "message": "Aleyk Salam" } -### Insert a document to storageName with custom id -PUT {{apiUrl}}/{{apiVersion}}/sample +### Edit document +PATCH {{apiUrl}}/{{apiVersion}}/?storage=comments/page1 authorization: Bearer {{token}} Content-Type: application/json { - "_id": "doc-id-2", + "_id": "c1", + "_updatedBy": "demo", "from": "Ali Mihandoost", "message": "Salam ;)" } -### Insert a document to storageName with custom id -DELETE {{apiUrl}}/{{apiVersion}}/sample/doc-id-2 +### Delete document +DELETE {{apiUrl}}/{{apiVersion}}/?storage=comments/page1&id=c1 authorization: Bearer {{token}} +### Get keys +GET {{apiUrl}}/{{apiVersion}}/keys?storage=comments/page1 +authorization: Bearer {{token}} + +### Get all data +GET {{apiUrl}}/{{apiVersion}}/all?storage=comments/page1 +authorization: Bearer {{token}} ### === Test other routes and errors === ### Page Home GET {{apiUrl}}/{{apiVersion}} -### Page 404 (wrong path) -GET {{apiUrl}}/{{apiVersion}}/jafang +### Page health +GET {{apiUrl}}/{{apiVersion}}/health + +### Get a document without token +GET {{apiUrl}}/{{apiVersion}}/?storage=comments/page1/page1&id=c1 ### Page 404 (wrong method) TRACE {{apiUrl}}/{{apiVersion}} -### Page health -GET {{apiUrl}}/{{apiVersion}}/health +### Get a document by storageName without id +GET {{apiUrl}}/{{apiVersion}}/?storage=comments/page1 +authorization: Bearer {{token}} ### empty body -POST {{apiUrl}}/{{apiVersion}} +PATCH {{apiUrl}}/{{apiVersion}}/?storage=comments/page1 +authorization: Bearer {{token}} Content-Type: application/json ### invalid json -POST {{apiUrl}}/{{apiVersion}}/echo +PATCH {{apiUrl}}/{{apiVersion}}/?storage=comments/page1 +authorization: Bearer {{token}} Content-Type: application/json { - "a": 1, - b: 2, + "_id": } diff --git a/packages/nanoservice/storage/package.json b/packages/nanoservice/storage/package.json index 821060a25..2eb3a8309 100644 --- a/packages/nanoservice/storage/package.json +++ b/packages/nanoservice/storage/package.json @@ -33,31 +33,30 @@ "scripts": { "l": "yarn lint", "b": "yarn build", - "cb": "npm-run-all --sequential clean build", - "s": "npm-run-all --sequential clean build serve", + "cb": "run-s clean build", + "s": "run-s clean build serve", "w": "yarn watch", "f": "yarn format", "fl": "yarn format:eslint", "fp": "yarn format:prettier", "start": "yarn serve", - "lint": "npm-run-all --sequential lint:*", + "lint": "run-s lint:*", "lint:ts": "eslint **/*.ts", - "build": "npm-run-all --sequential build:*", + "build": "run-s build:*", "build:ts": "tsc --build", - "format": "npm-run-all --sequential format:prettier format:eslint", + "format": "run-s format:prettier format:eslint", "format:eslint": "eslint **/*.ts --fix", "format:prettier": "prettier \"**/*.{html,json,md,ts}\" --ignore-path ./.eslintignore --write", "clean": "rimraf dist/", "serve": "node dist/index.js", "serve:debug": "node --inspect dist/index.js", - "watch": "npm-run-all --parallel watch:ts watch:node", - "watch:node": "nodemon dist/index.js", - "watch:debug-node": "nodemon --inspect dist/index.js", + "watch": "run-s clean build && run-p watch:ts watch:node", + "watch:node": "nodemon -w dist/ dist/index.js", + "watch:debug-node": "nodemon -w dist/ --inspect dist/index.js", "watch:ts": "yarn build:ts --watch --preserveWatchOutput" }, "dependencies": { "@alwatr/logger": "^0.21.0", - "@alwatr/math": "^0.21.0", "@alwatr/nano-server": "^0.21.0", "@alwatr/storage": "^0.21.0" }, diff --git a/packages/nanoservice/storage/src/index.ts b/packages/nanoservice/storage/src/index.ts index fd2fba83e..9289e522e 100644 --- a/packages/nanoservice/storage/src/index.ts +++ b/packages/nanoservice/storage/src/index.ts @@ -1,6 +1,8 @@ -import './route/home.js'; -import './route/update.js'; import './route/get.js'; +import './route/patch.js'; +import './route/delete.js'; +import './route/keys.js'; +import './route/all.js'; import {logger} from './lib/config.js'; logger.logOther('..:: Alwatr Storage Nanoservice API ::..'); diff --git a/packages/nanoservice/storage/src/lib/config.ts b/packages/nanoservice/storage/src/lib/config.ts index 23b8f6d89..2fc919bf5 100644 --- a/packages/nanoservice/storage/src/lib/config.ts +++ b/packages/nanoservice/storage/src/lib/config.ts @@ -1,13 +1,12 @@ import {createLogger} from '@alwatr/logger'; -import {isNumber} from '@alwatr/math'; + +export const logger = createLogger('service-storage'); export const config = { - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - port: isNumber(process.env.PORT) ? +process.env.PORT! : 80, + host: process.env.HOST ?? '0.0.0.0', + port: process.env.PORT != null ? +process.env.PORT : 80, storagePath: process.env.STORAGE_PATH ?? 'db', - dataModelName: process.env.DATA_MODEL_NAME ?? 'data-model-list', + token: process.env.TOKEN ?? 'alwatr_110_313', }; -export const logger = createLogger('service-storage'); - -logger.logProperty('config', config); +logger.logProperty('config', {...config, token: '***'}); diff --git a/packages/nanoservice/storage/src/lib/storage-provider.ts b/packages/nanoservice/storage/src/lib/storage-provider.ts index 5b3ec8e0e..e3bca1e41 100644 --- a/packages/nanoservice/storage/src/lib/storage-provider.ts +++ b/packages/nanoservice/storage/src/lib/storage-provider.ts @@ -2,35 +2,4 @@ import {AlwatrStorageProvider} from '@alwatr/storage/provider.js'; import {config} from './config.js'; -import type {DataModel} from './type.js'; - export const storageProvider = new AlwatrStorageProvider({path: config.storagePath}); - -const dataModelStorage = await storageProvider.get({ - name: config.dataModelName, - path: `${config.storagePath}/private`, -}); - -if (dataModelStorage.length === 0) { - dataModelStorage.set({ - _id: 'sample', - _updatedBy: 'system', - subFolder: 'public', - subStorage: false, - }); -} - -export function getDataModel(storageName: string): DataModel | null { - const splittedName = storageName.split('/'); - let testStorageName = ''; - for (let i = 0; i < splittedName.length; i++) { - testStorageName += splittedName[i]; - const dataModel = dataModelStorage.get(testStorageName, true); - if (dataModel != null) { - if (dataModel.subStorage === false && dataModel._id !== storageName) continue; - return dataModel; - } - testStorageName += '/'; - } - return null; -} diff --git a/packages/nanoservice/storage/src/lib/token.ts b/packages/nanoservice/storage/src/lib/token.ts deleted file mode 100644 index ef4edd0d2..000000000 --- a/packages/nanoservice/storage/src/lib/token.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type {AlwatrConnection} from '@alwatr/nano-server'; - -export function requireToken(connection: AlwatrConnection): string | null { - const token = connection.token; - - if (token == null) { - connection.reply({ - ok: false, - statusCode: 401, - errorCode: 'token_required', - }); - return null; - } - - // TODO: validate token - if (token.length < 32) { - connection.reply({ - ok: false, - statusCode: 403, - errorCode: 'token_not_valid', - }); - return null; - } - - return token; -} - -const subTokenLength = 12; -export const subToken = (token: string): string => { - return token.substring(0, subTokenLength); -}; diff --git a/packages/nanoservice/storage/src/lib/type.ts b/packages/nanoservice/storage/src/lib/type.ts deleted file mode 100644 index be235d803..000000000 --- a/packages/nanoservice/storage/src/lib/type.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {DocumentObject} from '@alwatr/storage'; - -export interface DataModel extends DocumentObject { - /** - * Save storage data in public or private sub folder. - */ - subFolder: 'public' | 'private'; - - /** - * Accept subStorage like `comments/page1`. - */ - subStorage: boolean; -} diff --git a/packages/nanoservice/storage/src/route/all.ts b/packages/nanoservice/storage/src/route/all.ts new file mode 100644 index 000000000..703f4993c --- /dev/null +++ b/packages/nanoservice/storage/src/route/all.ts @@ -0,0 +1,24 @@ +import {config, logger} from '../lib/config.js'; +import {nanoServer} from '../lib/nano-server.js'; +import {storageProvider} from '../lib/storage-provider.js'; + +import type {AlwatrConnection} from '@alwatr/nano-server'; + +nanoServer.route('GET', '/all', getAllDocument); + +async function getAllDocument(connection: AlwatrConnection): Promise { + logger.logMethod('getAllDocument'); + + const token = connection.requireToken(config.token); + if (token == null) return; + + const params = connection.requireQueryParams<{storage: string}>({storage: 'string'}); + if (params == null) return; + + const storage = storageProvider.get({name: params.storage}); + + connection.reply({ + ok: true, + data: storage._data, + }); +} diff --git a/packages/nanoservice/storage/src/route/delete.ts b/packages/nanoservice/storage/src/route/delete.ts new file mode 100644 index 000000000..b516dc3a2 --- /dev/null +++ b/packages/nanoservice/storage/src/route/delete.ts @@ -0,0 +1,33 @@ +import {config, logger} from '../lib/config.js'; +import {nanoServer} from '../lib/nano-server.js'; +import {storageProvider} from '../lib/storage-provider.js'; + +import type {AlwatrConnection} from '@alwatr/nano-server'; + +nanoServer.route('DELETE', 'all', removeDocument); + +async function removeDocument(connection: AlwatrConnection): Promise { + logger.logMethodArgs('updateDocument', {method: connection.method}); + + const token = connection.requireToken(config.token); + if (token == null) return; + + const param = connection.requireQueryParams<{storage: string, id: string}>({storage: 'string', id: 'string'}); + if (param === null) return; + + const storage = storageProvider.get({name: param.storage}); + + if (storage.delete(param.id) === true) { + connection.reply({ + ok: true, + data: {}, + }); + } + else { + connection.reply({ + ok: false, + statusCode: 404, + errorCode: 'document_not_found', + }); + } +} diff --git a/packages/nanoservice/storage/src/route/get.ts b/packages/nanoservice/storage/src/route/get.ts index 87c2f0b88..3c7147939 100644 --- a/packages/nanoservice/storage/src/route/get.ts +++ b/packages/nanoservice/storage/src/route/get.ts @@ -1,52 +1,35 @@ import {config, logger} from '../lib/config.js'; import {nanoServer} from '../lib/nano-server.js'; -import {storageProvider, getDataModel} from '../lib/storage-provider.js'; -import {requireToken} from '../lib/token.js'; +import {storageProvider} from '../lib/storage-provider.js'; import type {AlwatrConnection} from '@alwatr/nano-server'; -nanoServer.route('GET', 'all', getDocument); +nanoServer.route('GET', '/', getDocument); -async function getDocument(connection: AlwatrConnection): Promise { - const splittedPath = connection.url.pathname - .substring(1) // remove the first `/` - .split('/'); +function getDocument(connection: AlwatrConnection): void { + logger.logMethod('getDocument'); - if (splittedPath.length < 2) { + if (!connection.url.search) { return connection.reply({ - ok: false, - statusCode: 400, - errorCode: 'invalid_path_format', + ok: true, + data: { + app: 'Alwatr Storage Server', + message: 'Hello ;)', + }, }); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const documentId = splittedPath.pop()!; - const storageName = splittedPath.join('/'); - - logger.logMethodArgs('getDocument', {storageName, documentId}); - - const token = requireToken(connection); - if (token === null) return; + const token = connection.requireToken(config.token); + if (token == null) return; - const storageModel = getDataModel(storageName); - - if (storageModel === null) { - return connection.reply({ - ok: false, - statusCode: 404, - errorCode: 'storage_not_defined', - }); - } + const params = connection.requireQueryParams<{storage: string, id: string}>({storage: 'string', id: 'string'}); + if (params == null) return; - const storage = storageProvider.get({ - name: storageName, - path: `${config.storagePath}/${storageModel.subFolder}`, - }); + const storage = storageProvider.get({name: params.storage}); - const document = storage.get(documentId, true); + const document = storage.get(params.id, true); - if (document === null) { + if (document == null) { return connection.reply({ ok: false, statusCode: 404, diff --git a/packages/nanoservice/storage/src/route/home.ts b/packages/nanoservice/storage/src/route/home.ts deleted file mode 100644 index 82671895e..000000000 --- a/packages/nanoservice/storage/src/route/home.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {nanoServer} from '../lib/nano-server.js'; - -nanoServer.route('GET', '/', async (connection) => { - connection.reply({ - ok: true, - data: { - app: 'Alwatr Storage Nanoservice API', - message: 'Hello ;)', - }, - }); -}); diff --git a/packages/nanoservice/storage/src/route/keys.ts b/packages/nanoservice/storage/src/route/keys.ts new file mode 100644 index 000000000..5143211be --- /dev/null +++ b/packages/nanoservice/storage/src/route/keys.ts @@ -0,0 +1,24 @@ +import {config, logger} from '../lib/config.js'; +import {nanoServer} from '../lib/nano-server.js'; +import {storageProvider} from '../lib/storage-provider.js'; + +import type {AlwatrConnection} from '@alwatr/nano-server'; + +nanoServer.route('GET', '/keys', getStorageKeys); + +async function getStorageKeys(connection: AlwatrConnection): Promise { + logger.logMethod('getStorageKeys'); + + const token = connection.requireToken(config.token); + if (token == null) return; + + const params = connection.requireQueryParams<{storage: string}>({storage: 'string'}); + if (params == null) return; + + const storage = storageProvider.get({name: params.storage}); + + connection.reply({ + ok: true, + data: {keys: storage.keys}, + }); +} diff --git a/packages/nanoservice/storage/src/route/patch.ts b/packages/nanoservice/storage/src/route/patch.ts new file mode 100644 index 000000000..59f4ab2ec --- /dev/null +++ b/packages/nanoservice/storage/src/route/patch.ts @@ -0,0 +1,38 @@ +import {config, logger} from '../lib/config.js'; +import {nanoServer} from '../lib/nano-server.js'; +import {storageProvider} from '../lib/storage-provider.js'; + +import type {AlwatrConnection} from '@alwatr/nano-server'; +import type {DocumentObject} from '@alwatr/storage'; + +nanoServer.route('PATCH', 'all', updateDocument); + +async function updateDocument(connection: AlwatrConnection): Promise { + logger.logMethod('updateDocument'); + + const token = connection.requireToken(config.token); + if (token == null) return; + + const param = connection.requireQueryParams<{storage: string}>({storage: 'string'}); + if (param === null) return; + + const document = await connection.requireJsonBody(); + if (document == null) return; + + if (!(typeof document._id === 'string' && document._id.length !== 0)) { + return connection.reply({ + ok: false, + statusCode: 406, + errorCode: 'doc_id_required', + }); + } + + document._updatedBy ??= 'admin'; + + const storage = storageProvider.get({name: param.storage}); + + connection.reply({ + ok: true, + data: storage.set(document, true), + }); +} diff --git a/packages/nanoservice/storage/src/route/update.ts b/packages/nanoservice/storage/src/route/update.ts deleted file mode 100644 index 8d03c5606..000000000 --- a/packages/nanoservice/storage/src/route/update.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {config, logger} from '../lib/config.js'; -import {nanoServer} from '../lib/nano-server.js'; -import {storageProvider, getDataModel} from '../lib/storage-provider.js'; -import {requireToken, subToken} from '../lib/token.js'; - -import type {AlwatrConnection} from '@alwatr/nano-server'; -import type {DocumentObject} from '@alwatr/storage'; - -nanoServer.route('POST', 'all', updateDocument); - -async function updateDocument(connection: AlwatrConnection): Promise { - const storageName = connection.url.pathname.substring(1); // remove the first `/` - logger.logMethodArgs('updateDocument', {storageName}); - - const bodyData = await connection.requireJsonBody(); - const token = requireToken(connection); - if (bodyData === null || token === null) return; - - if (typeof bodyData._id != 'string') { - return connection.reply({ - ok: false, - statusCode: 406, - errorCode: '_id required', - }); - } - - const storageModel = getDataModel(storageName); - - if (storageModel === null) { - return connection.reply({ - ok: false, - statusCode: 404, - errorCode: 'storage_not_defined', - data: { - storageName, - documentId: bodyData._id, - }, - }); - } - - const storage = storageProvider.get({ - name: storageName, - path: `${config.storagePath}/${storageModel.subFolder}`, - }); - - const document: DocumentObject = { - ...bodyData, // TODO: validate keys - _id: bodyData._id, - _updatedBy: subToken(token), - }; - - storage.set(document, true); - - connection.reply({ - ok: true, - data: document, - }); -}