From 9ba2ab86c7654299b93e5020408ea96d3de6d2b6 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 09:33:58 -0500 Subject: [PATCH 01/33] testing variable syntax --- docs/book.json | 6 ++++++ docs/getting-started/getting-started.md | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 docs/book.json diff --git a/docs/book.json b/docs/book.json new file mode 100644 index 00000000..7789ee19 --- /dev/null +++ b/docs/book.json @@ -0,0 +1,6 @@ +{ + "root": "./docs", + "variables": { + "node_version": "16.17.1" + } +} \ No newline at end of file diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index e4d75c4e..3191994d 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,3 +1,5 @@ +{{node_version}} {{ node_version }} + # Getting Started HarperDB was built by developers for developers, which means getting started should be simple. From 143bf62a1b3c25fc4f3a06d07b704ae566686634 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 10:02:34 -0500 Subject: [PATCH 02/33] testing variable syntax --- docs/book.json | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/book.json b/docs/book.json index 7789ee19..86276ea7 100644 --- a/docs/book.json +++ b/docs/book.json @@ -1,5 +1,4 @@ { - "root": "./docs", "variables": { "node_version": "16.17.1" } From 3c119d916636de2c8fa1b449ea2ff6c9fc754a2f Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 10:03:47 -0500 Subject: [PATCH 03/33] testing variable syntax --- docs/getting-started/getting-started.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index 3191994d..8ad30989 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,4 +1,5 @@ {{node_version}} {{ node_version }} +book.[node_version] # Getting Started From 2aec8f98deb6a4ae89aa827c0b22472f3b61b3ab Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 10:17:50 -0500 Subject: [PATCH 04/33] testing variable syntax --- book.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 book.json diff --git a/book.json b/book.json new file mode 100644 index 00000000..86276ea7 --- /dev/null +++ b/book.json @@ -0,0 +1,5 @@ +{ + "variables": { + "node_version": "16.17.1" + } +} \ No newline at end of file From 1f3b4f03ecea01e612c711ed4daccc54c7b92454 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 11:00:46 -0500 Subject: [PATCH 05/33] testing variable syntax --- book.json | 1 + 1 file changed, 1 insertion(+) diff --git a/book.json b/book.json index 86276ea7..0c177971 100644 --- a/book.json +++ b/book.json @@ -1,4 +1,5 @@ { + "root": "/docs", "variables": { "node_version": "16.17.1" } From d2603a7adf7c84f055b7cd82767a9c79a8e28142 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 12:13:02 -0500 Subject: [PATCH 06/33] testing variable syntax --- book.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book.json b/book.json index 0c177971..fbe8eeba 100644 --- a/book.json +++ b/book.json @@ -1,5 +1,5 @@ { - "root": "/docs", + "root": "docs", "variables": { "node_version": "16.17.1" } From 81f4d2ae77bafffe70e9daf8e8cf39709166022d Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 12:14:41 -0500 Subject: [PATCH 07/33] testing variable syntax --- book.json | 1 - docs/book.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/book.json b/book.json index fbe8eeba..86276ea7 100644 --- a/book.json +++ b/book.json @@ -1,5 +1,4 @@ { - "root": "docs", "variables": { "node_version": "16.17.1" } diff --git a/docs/book.json b/docs/book.json index 86276ea7..0fd0e93c 100644 --- a/docs/book.json +++ b/docs/book.json @@ -1,4 +1,5 @@ { + "root": "../docs", "variables": { "node_version": "16.17.1" } From 79d2562bd695a65627180cf9d7cbc6906d447f88 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 12:26:02 -0500 Subject: [PATCH 08/33] testing variable syntax --- docs/book.json | 3 +-- docs/getting-started/getting-started.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/book.json b/docs/book.json index 0fd0e93c..6e486d47 100644 --- a/docs/book.json +++ b/docs/book.json @@ -1,6 +1,5 @@ { - "root": "../docs", "variables": { - "node_version": "16.17.1" + "nodeVersion": "16.17.1" } } \ No newline at end of file diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index 8ad30989..220f2459 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,4 +1,4 @@ -{{node_version}} {{ node_version }} +{{nodeVersion}} {{ nodeVersion }} book.[node_version] # Getting Started From 9cc4e3fa32be583c33ecdfb2d7ee86dfe14940a0 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 13:04:31 -0500 Subject: [PATCH 09/33] testing variable syntax --- book.json | 1 + docs/book.json | 1 + docs/getting-started/getting-started.md | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/book.json b/book.json index 86276ea7..fbe8eeba 100644 --- a/book.json +++ b/book.json @@ -1,4 +1,5 @@ { + "root": "docs", "variables": { "node_version": "16.17.1" } diff --git a/docs/book.json b/docs/book.json index 6e486d47..5e9a7443 100644 --- a/docs/book.json +++ b/docs/book.json @@ -1,4 +1,5 @@ { + "root": "../docs", "variables": { "nodeVersion": "16.17.1" } diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index 220f2459..1b072240 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,5 +1,6 @@ -{{nodeVersion}} {{ nodeVersion }} -book.[node_version] +{% set node_version = "16.17.1" %} + +{{node_version}} {{ node_version }} # Getting Started From 018a08871bb86e189ddb214f23a27fcb0239f1de Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 13:06:29 -0500 Subject: [PATCH 10/33] testing variable syntax --- docs/book.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book.json b/docs/book.json index 5e9a7443..7789ee19 100644 --- a/docs/book.json +++ b/docs/book.json @@ -1,6 +1,6 @@ { - "root": "../docs", + "root": "./docs", "variables": { - "nodeVersion": "16.17.1" + "node_version": "16.17.1" } } \ No newline at end of file From 3375c62115abf2f43188b2ac632cf8f72fb5aadc Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 13:09:27 -0500 Subject: [PATCH 11/33] testing variable syntax --- docs/getting-started/getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index 1b072240..da6a5d24 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,6 +1,6 @@ -{% set node_version = "16.17.1" %} +{% set nodeVersion = "16.17.1" %} -{{node_version}} {{ node_version }} +{{nodeVersion}} {{ nodeVersion }} # Getting Started From 8abcd22d890131fc24cc99305f06ac53d156824b Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 5 Oct 2022 13:12:42 -0500 Subject: [PATCH 12/33] testing variable syntax --- docs/getting-started/getting-started.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index da6a5d24..af6d5506 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -2,6 +2,9 @@ {{nodeVersion}} {{ nodeVersion }} +gitbook.time +gitbook.version + # Getting Started HarperDB was built by developers for developers, which means getting started should be simple. From 7abd4367d678a1c9b6e09101ede5719f79580ee6 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Thu, 6 Oct 2022 10:20:19 -0500 Subject: [PATCH 13/33] testing variable syntax --- .gitbook.yaml | 4 ++++ book.json | 6 ------ docs/.gitbook.yaml | 4 ++++ docs/book.json | 6 ------ docs/getting-started/getting-started.md | 7 ++++--- 5 files changed, 12 insertions(+), 15 deletions(-) create mode 100644 .gitbook.yaml delete mode 100644 book.json create mode 100644 docs/.gitbook.yaml delete mode 100644 docs/book.json diff --git a/.gitbook.yaml b/.gitbook.yaml new file mode 100644 index 00000000..68abcbf7 --- /dev/null +++ b/.gitbook.yaml @@ -0,0 +1,4 @@ +root: docs, +variables: + nodeVersion: 16.17.1 + diff --git a/book.json b/book.json deleted file mode 100644 index fbe8eeba..00000000 --- a/book.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "root": "docs", - "variables": { - "node_version": "16.17.1" - } -} \ No newline at end of file diff --git a/docs/.gitbook.yaml b/docs/.gitbook.yaml new file mode 100644 index 00000000..5a0ec332 --- /dev/null +++ b/docs/.gitbook.yaml @@ -0,0 +1,4 @@ +root: ./docs, +variables: { + nodeVersion: 16.17.1 + } diff --git a/docs/book.json b/docs/book.json deleted file mode 100644 index 7789ee19..00000000 --- a/docs/book.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "root": "./docs", - "variables": { - "node_version": "16.17.1" - } -} \ No newline at end of file diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index af6d5506..bb1f1b49 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,9 +1,10 @@ -{% set nodeVersion = "16.17.1" %} +[//]: # ({% set nodeVersion = "16.17.1" %}) {{nodeVersion}} {{ nodeVersion }} -gitbook.time -gitbook.version +[//]: # (gitbook.time) + +[//]: # (gitbook.version) # Getting Started From 1415f76564d9066dc06b7822720ea5ff95d5ac67 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Thu, 6 Oct 2022 10:24:40 -0500 Subject: [PATCH 14/33] testing variable syntax --- .gitbook.yaml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .gitbook.yaml diff --git a/.gitbook.yaml b/.gitbook.yaml deleted file mode 100644 index 68abcbf7..00000000 --- a/.gitbook.yaml +++ /dev/null @@ -1,4 +0,0 @@ -root: docs, -variables: - nodeVersion: 16.17.1 - From d2dd1fe55f7f24c49f15f8a67c845899a3d5cb53 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Thu, 6 Oct 2022 10:26:25 -0500 Subject: [PATCH 15/33] testing variable syntax --- docs/.gitbook.yaml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 docs/.gitbook.yaml diff --git a/docs/.gitbook.yaml b/docs/.gitbook.yaml deleted file mode 100644 index 5a0ec332..00000000 --- a/docs/.gitbook.yaml +++ /dev/null @@ -1,4 +0,0 @@ -root: ./docs, -variables: { - nodeVersion: 16.17.1 - } From 3f7618efcfc04649e018ee768578f3910828efc0 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Thu, 6 Oct 2022 10:29:57 -0500 Subject: [PATCH 16/33] testing variable syntax --- docs/book.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/book.json diff --git a/docs/book.json b/docs/book.json new file mode 100644 index 00000000..7d5e4a87 --- /dev/null +++ b/docs/book.json @@ -0,0 +1,6 @@ +{ + "root": "./docs", + "variables": { + "nodeVersion": "16.17.1" + } +} \ No newline at end of file From 501405bd6c035ffd7baae78bb1a9d0b0a3d55607 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Thu, 6 Oct 2022 13:42:37 -0500 Subject: [PATCH 17/33] reverted changes from variable testing --- docs/book.json | 6 ------ docs/getting-started/getting-started.md | 8 -------- 2 files changed, 14 deletions(-) delete mode 100644 docs/book.json diff --git a/docs/book.json b/docs/book.json deleted file mode 100644 index 7d5e4a87..00000000 --- a/docs/book.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "root": "./docs", - "variables": { - "nodeVersion": "16.17.1" - } -} \ No newline at end of file diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md index bb1f1b49..e4d75c4e 100644 --- a/docs/getting-started/getting-started.md +++ b/docs/getting-started/getting-started.md @@ -1,11 +1,3 @@ -[//]: # ({% set nodeVersion = "16.17.1" %}) - -{{nodeVersion}} {{ nodeVersion }} - -[//]: # (gitbook.time) - -[//]: # (gitbook.version) - # Getting Started HarperDB was built by developers for developers, which means getting started should be simple. From 414c698c9c508fb8b5652a2a6c69f3ffbdb845fe Mon Sep 17 00:00:00 2001 From: terraHDB Date: Tue, 18 Oct 2022 22:41:53 -0500 Subject: [PATCH 18/33] docs migration: 1 of 2 custom functions --- docs/custom-functions/create-project.md | 30 ++++ docs/custom-functions/define-helpers.md | 62 +++++++++ docs/custom-functions/define-routes.md | 129 ++++++++++++++++++ docs/custom-functions/host-static.md | 27 ++++ docs/custom-functions/index.md | 17 +++ .../requirements-definitions.md | 59 ++++++++ 6 files changed, 324 insertions(+) create mode 100644 docs/custom-functions/create-project.md create mode 100644 docs/custom-functions/define-helpers.md create mode 100644 docs/custom-functions/define-routes.md create mode 100644 docs/custom-functions/host-static.md create mode 100644 docs/custom-functions/requirements-definitions.md diff --git a/docs/custom-functions/create-project.md b/docs/custom-functions/create-project.md new file mode 100644 index 00000000..e84ac8b1 --- /dev/null +++ b/docs/custom-functions/create-project.md @@ -0,0 +1,30 @@ +# Create a Project +The easiest way to manage Custom Functions is through [HarperDB Studio](https://harperdb.io/docs/harperdb-studio/manage-functions/). You can read more about [managing Custom Functions through the HarperDB Studio here](https://harperdb.io/docs/harperdb-studio/manage-functions/). + +For users who prefer to handle project creation manually, you have three options: + + + +1. **use the add_custom_function_project operation** +This operation creates a new project folder for you, and populates it with templates for the routes, helpers, and static subfolders. + +```json +{ +"operation": "add_custom_function_project", +"project": "dogs" +} +``` + + +2. **clone our public github project template** + +```bash +> git clone https://github.com/HarperDB/harperdb-custom-functions-template.git ~/hdb/custom_functions/dogs +``` + + +3. **create a project folder in your Custom Functions root directory** + +```bash +> mkdir ~/hdb/custom_functions/dogs +``` \ No newline at end of file diff --git a/docs/custom-functions/define-helpers.md b/docs/custom-functions/define-helpers.md new file mode 100644 index 00000000..0eafa2c2 --- /dev/null +++ b/docs/custom-functions/define-helpers.md @@ -0,0 +1,62 @@ +# Define Helpers + +Helpers are functions for use within your routes. You may want to use the same helper in multiple route files, so this allows you to write it once, and include it at wherever you need it. + + + +* To use your helpers, they must be exported from your helper file using `module.exports`. + +* To include the helper in your route file, you must use a `require` statement. + + +Below is code from the customValidation helper that is referenced in [Define Routes](https://harperdb.io/developers/documentation/custom-functions/define-routes/). It takes the request and the logger method from the route declaration, and makes a call to an external API to validate the headers. The API in this example is just returning a list of ToDos, but it could easily be replaced with a call to a real authentication service. + + +```javascript +const authRequest = (options) => { + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + res.setEncoding('utf8'); + let responseBody = ''; + + res.on('data', (chunk) => { + responseBody += chunk; + }); + + res.on('end', () => { + resolve(JSON.parse(responseBody)); + }); + }); + + req.on('error', (err) => { + reject(err); + }); + + req.end(); + }); +}; + +const customValidation = async (request,logger) => { + const options = { + hostname: 'jsonplaceholder.typicode.com', + port: 443, + path: '/todos/1', + method: 'GET', + headers: { authorization: request.headers.authorization }, + }; + + const result = await authRequest(options); + + /* + * throw an authentication error based on the response body or statusCode + */ + if (result.error) { + const errorString = result.error || 'Sorry, there was an error authenticating your request'; + logger.error(errorString); + throw new Error(errorString); + } + return request; +}; + +module.exports = customValidation; +``` diff --git a/docs/custom-functions/define-routes.md b/docs/custom-functions/define-routes.md new file mode 100644 index 00000000..dcb3e983 --- /dev/null +++ b/docs/custom-functions/define-routes.md @@ -0,0 +1,129 @@ +# Define Routes + +HarperDB’s Custom Functions is built on top of [Fastify](https://www.fastify.io/), so our route definitions follow their specifications. Below is a very simple example of a route declaration. + + + +Route URLs are resolved in the following manner: + +* [**Instance URL**]:[**Custom Functions Port**]/[**Project Name**]/[**Route URL**] + +* The route below, within the **dogs** project, would be available at **http://localhost:9926/dogs**. + + +In effect, this route is just a pass-through to HarperDB. The same result could have been achieved by hitting the core HarperDB API, since it uses **hdbCore.preValidation** and **hdbCore.request**, which are defined in the “helper methods” section, below. + + + +```javascript +module.exports = async (server, { hdbCore, logger }) => { + server.route({ + url: '/', + method: 'POST', + preValidation: hdbCore.preValidation, + handler: hdbCore.request, + }) +} +``` + + +## Custom Handlers + +For endpoints where you want to execute multiple operations against HarperDB, or perform additional processing (like an ML classification, or an aggregation, or a call to a 3rd party API), you can define your own logic in the handler. The function below will execute a query against the dogs table, and filter the results to only return those dogs over 4 years in age. + + + +**IMPORTANT: This route has NO preValidation and uses hdbCore.requestWithoutAuthentication, which- as the name implies- bypasses all user authentication. See the security concerns and mitigations in the “helper methods” section, below.** + + + +```javascript +module.exports = async (server, { hdbCore, logger }) => { + server.route({ + url: '/:id', + method: 'GET', + handler: (request) => { + request.body= { + operation: 'sql', + sql: `SELECT * FROM dev.dog WHERE id = ${request.params.id}` + }; + + const result = await hdbCore.requestWithoutAuthentication(request); + return result.filter((dog) => dog.age > 4); + } + }); +} +``` + +## Custom preValidation Hooks +The simple example above was just a pass-through to HarperDB- the exact same result could have been achieved by hitting the core HarperDB API. But for many applications, you may want to authenticate the user using custom logic you write, or by conferring with a 3rd party service. Custom preValidation hooks let you do just that. + + + +Below is an example of a route that uses a custom Validation hook: + +```javascript +const customValidation = require('../helpers/customValidation'); + +module.exports = async (server, { hdbCore, logger }) => { + server.route({ + url: '/:id', + method: 'GET', + preValidation: (request) => customValidation(request, logger), + handler: (request) => { + request.body= { + operation: 'sql', + sql: `SELECT * FROM dev.dog WHERE id = ${request.params.id}` + }; + + return hdbCore.requestWithoutAuthentication(request); + } + }); +} +``` + + +Notice we imported customValidation from the **helpers** directory. To include a helper, and to see the actual code within customValidation, see [Define Helpers](https://harperdb.io/developers/documentation/custom-functions/define-helpers/). + +## Helper Methods +When declaring routes, you are given access to 2 helper methods: hdbCore and logger. + + + +**hdbCore** + +hdbCore contains three functions that allow you to authenticate an inbound request, and execute operations against HarperDB directly, by passing the standard Operations API. + + + +* **preValidation** + + This takes the authorization header from the inbound request and executes the same authentication as the standard HarperDB Operation API. It will determine if the user exists, and if they are allowed to perform this operation. **The request method defined below must be the handler when you use this preValidation hook**. + +* **request** + + Executes a request against HarperDB. It bypasses the Operations API, and uses the request.body, which should contain a standard HarperDB operation. + +* **requestWithoutAuthentication** + + Executes a request against HarperDB without any security checks around whether the inbound user is allowed to make this request. For security purposes, you should always take the following precautions when using this method: + + * **never pass through a user-submitted operation.** Always define your operation in your handler. A user could send an operation like drop_schema, and hdbCore.requestWithoutAuthentication will let them do it. + + * **never use user-submitted values in your operations, including url params.** Always validate user input to ensure nothing malformed or malicious becomes part of the operation you send to hdbCore.requestWithoutAuthentication. + + +**logger** + +This helper allows you to write directly to the HarperDB log. It’s useful for debugging during development. There are 5 functions contained within logger, each of which pertains to a different **LOG_LEVEL** configuration in your harperdb-config.yaml file. + + +* logger.trace(‘Starting the handler for /dogs’) + +* logger.debug(‘This should only fire once’) + +* logger.warn(‘This should never ever fire’) + +* logger.error(‘This did not go well’) + +* logger.fatal(‘This did not go very well at all’) \ No newline at end of file diff --git a/docs/custom-functions/host-static.md b/docs/custom-functions/host-static.md new file mode 100644 index 00000000..a0ba8ad1 --- /dev/null +++ b/docs/custom-functions/host-static.md @@ -0,0 +1,27 @@ +# Host A Static Web UI +We’ve configured the Custom Functions server to automatically serve any static UI files it finds in a project’s **/static** folder. + + +In order to serve your static UI correctly, your UI project must meet the following requirements: + +* Have an index file located at **/static/index.html** + +* Correctly path any other files relative to index.html + +* If your app makes use of client-side routing, it must have **[project_name]/static** as its base (`basename` for react-router, `base` for vue-router, etc.): + +```javascript + + + + + + +``` + + +Supporting files, like css, js, and images may be located within subfolders or at the root of the **/static** folder, whichever you prefer. + + + +If you’re using a framework (React, Vue, etc.), we recommend dropping the output of your build process (your `dist` or `build` folder) directly into your project’s **/static** folder. The output of the build process is usually optimized and compressed, and will be more performant than raw source code. \ No newline at end of file diff --git a/docs/custom-functions/index.md b/docs/custom-functions/index.md index 79509c06..aee6d649 100644 --- a/docs/custom-functions/index.md +++ b/docs/custom-functions/index.md @@ -1 +1,18 @@ # Custom Functions + +* Add your own API endpoints to a standalone API server inside HarperDB + +* Use HarperDB Core methods to interact with your data at lightning speed + +* Custom Functions are powered by Fastify, so they’re extremely flexible + +* Manage in HarperDB Studio, or use your own IDE and Version Management System + +* Distribute your Custom Functions to all your HarperDB instances with a single click + +--- +* [Create A Project](https://harperdb.io/docs/custom-functions/create-a-project/) + +* [Define Routes](https://harperdb.io/docs/custom-functions/define-routes/) + +* [Host a Static UI](https://harperdb.io/docs/custom-functions/host-a-static-web-ui/) \ No newline at end of file diff --git a/docs/custom-functions/requirements-definitions.md b/docs/custom-functions/requirements-definitions.md new file mode 100644 index 00000000..2a66cce6 --- /dev/null +++ b/docs/custom-functions/requirements-definitions.md @@ -0,0 +1,59 @@ +# Requirements And Definitions +Before you get started with Custom Functions, here’s a primer on Custom Functions’ basic configuration and the structure of a Custom Functions Project. + +## Availability +Custom Functions is available in HarperDB version 3.1.0 and later. + +## Configuration +Custom Functions is enabled by default. If you wish to manage Custom Functions directly, you will find the relevant configuration parameters in your [configuration file](https://harperdb.io/docs/reference/configuration-file/). The available settings are listed below: + + + +* **customFunctions.enabled** + A boolean value that tells HarperDB to start the Custom Functions server. Set it to **true** to enable custom functions and **false** to disable. + +* **customFunctions.network.port** + This is the port HarperDB will use to start a standalone Fastify Server dedicated to serving your Custom Functions’ routes. + +* **customFunctions.root** + This is the root directory where your Custom Functions projects and their files will live. By default, it’s in your ~/hdb folder, but you can locate it anywhere- in a Developer folder next to your other development projects, for example. + +* **customFunctions.processes** + The number of processes you wish to spin up for the Custom Functions Fastify server. + +## Project Structure +**project folder** + +The name of the folder that holds your project files serves as the root prefix for all the routes you create. All routes created in the **dogs** project folder will have a URL like this: **https://my-server-url.com:9926/dogs/my/route**. As such, it’s important that any project folders you create avoid any characters that aren’t URL-friendly. If you stick to lowercase letters and hyphens, you should be fine. + + +**/routes folder** + +Files in the **routes** folder define the requests that your Custom Functions server will handle. They are [standard Fastify route declarations](https://www.fastify.io/docs/latest/Routes/), so if you’re familiar with them, you should be up and running in no time. The default components for a route are the url, method, preValidation, and handler. + +```javascript +module.exports = async (server, { hdbCore, logger }) => { + server.route({ + url: '/', + method: 'POST', + preValidation: hdbCore.preValidation, + handler: hdbCore.request, + }); +} +``` + +**/helpers folder** + +These files are plain javascript functions that you can use in your handlers, or for custom `preValidation` hooks. Examples include calls to third party Authentication services, filters for results of calls to HarperDB, and custom error responses. You must export them as a **module**, and you import them using **require**. + +```javascript +"use strict"; + +const dbFilter = (databaseResultsArray) => databaseResultsArray.filter((result) => result.showToApi === true); + +module.exports = dbFilter; +``` + +**/static folder** + +If you’d like to serve your visitors a static website, you can place the html and supporting files into a directory called **static**. The directory must have an **index.html** file, and can have as many supporting resources as are necessary in whatever subfolder structure you prefer within that **static** directory. \ No newline at end of file From f2be4fe308ec3f2ff56f45ed859437ea21dc5cfa Mon Sep 17 00:00:00 2001 From: terraHDB Date: Sun, 23 Oct 2022 13:30:16 -0500 Subject: [PATCH 19/33] corrections to 1/2 custom functions docs --- docs/custom-functions/create-project.md | 14 ++++++++------ docs/custom-functions/define-helpers.md | 6 +++--- docs/custom-functions/define-routes.md | 6 +++--- docs/custom-functions/host-static.md | 4 ++-- docs/custom-functions/requirements-definitions.md | 11 ++++------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/custom-functions/create-project.md b/docs/custom-functions/create-project.md index e84ac8b1..493ebea1 100644 --- a/docs/custom-functions/create-project.md +++ b/docs/custom-functions/create-project.md @@ -1,22 +1,24 @@ # Create a Project -The easiest way to manage Custom Functions is through [HarperDB Studio](https://harperdb.io/docs/harperdb-studio/manage-functions/). You can read more about [managing Custom Functions through the HarperDB Studio here](https://harperdb.io/docs/harperdb-studio/manage-functions/). +One easy way to manage Custom Functions is through [HarperDB Studio](https://harperdb.io/docs/harperdb-studio/manage-functions/). You can read more about [managing Custom Functions through the HarperDB Studio here](https://harperdb.io/docs/harperdb-studio/manage-functions/). -For users who prefer to handle project creation manually, you have three options: +To manually create a project, you have three options: 1. **use the add_custom_function_project operation** -This operation creates a new project folder for you, and populates it with templates for the routes, helpers, and static subfolders. + + This operation creates a new project folder, and populates it with templates for the routes, helpers, and static subfolders. ```json { -"operation": "add_custom_function_project", -"project": "dogs" + "operation": "add_custom_function_project", + "project": "dogs" } ``` -2. **clone our public github project template** +2. **clone our public gitHub project template** + ```bash > git clone https://github.com/HarperDB/harperdb-custom-functions-template.git ~/hdb/custom_functions/dogs diff --git a/docs/custom-functions/define-helpers.md b/docs/custom-functions/define-helpers.md index 0eafa2c2..f88a0633 100644 --- a/docs/custom-functions/define-helpers.md +++ b/docs/custom-functions/define-helpers.md @@ -1,12 +1,12 @@ # Define Helpers -Helpers are functions for use within your routes. You may want to use the same helper in multiple route files, so this allows you to write it once, and include it at wherever you need it. +Helpers are functions for use within your routes. You may want to use the same helper in multiple route files, so this allows you to write it once, and include it wherever you need it. -* To use your helpers, they must be exported from your helper file using `module.exports`. +* To use your helpers, they must be exported from your helper file. Please use any standard export mechanisms available for your module system. We like ESM, ECMAScript Modules. Our example below exports using `module.exports`. -* To include the helper in your route file, you must use a `require` statement. +* To include the helper in your route file, you must import. With ESM, you'd use a `require` statement. Below is code from the customValidation helper that is referenced in [Define Routes](https://harperdb.io/developers/documentation/custom-functions/define-routes/). It takes the request and the logger method from the route declaration, and makes a call to an external API to validate the headers. The API in this example is just returning a list of ToDos, but it could easily be replaced with a call to a real authentication service. diff --git a/docs/custom-functions/define-routes.md b/docs/custom-functions/define-routes.md index dcb3e983..b6ed6fcc 100644 --- a/docs/custom-functions/define-routes.md +++ b/docs/custom-functions/define-routes.md @@ -98,11 +98,11 @@ hdbCore contains three functions that allow you to authenticate an inbound reque * **preValidation** - This takes the authorization header from the inbound request and executes the same authentication as the standard HarperDB Operation API. It will determine if the user exists, and if they are allowed to perform this operation. **The request method defined below must be the handler when you use this preValidation hook**. + This takes the authorization header from the inbound request and executes the same authentication as the standard HarperDB Operations API. It will determine if the user exists, and if they are allowed to perform this operation. **If you use the request method, you have to use preValidation to get the authenticated user**. * **request** - Executes a request against HarperDB. It bypasses the Operations API, and uses the request.body, which should contain a standard HarperDB operation. + Executes a request against HarperDB. It calls the operations API, with authorization. * **requestWithoutAuthentication** @@ -115,7 +115,7 @@ hdbCore contains three functions that allow you to authenticate an inbound reque **logger** -This helper allows you to write directly to the HarperDB log. It’s useful for debugging during development. There are 5 functions contained within logger, each of which pertains to a different **LOG_LEVEL** configuration in your harperdb-config.yaml file. +This helper allows you to write directly to the Custom Functions log file, custom_functions.log. It’s useful for debugging during development, although you may also use the console logger. There are 5 functions contained within logger, each of which pertains to a different **logging.level** configuration in your harperdb-config.yaml file. * logger.trace(‘Starting the handler for /dogs’) diff --git a/docs/custom-functions/host-static.md b/docs/custom-functions/host-static.md index a0ba8ad1..4616b6b3 100644 --- a/docs/custom-functions/host-static.md +++ b/docs/custom-functions/host-static.md @@ -8,10 +8,10 @@ In order to serve your static UI correctly, your UI project must meet the follow * Correctly path any other files relative to index.html -* If your app makes use of client-side routing, it must have **[project_name]/static** as its base (`basename` for react-router, `base` for vue-router, etc.): +* If your app makes use of client-side routing, it must have **[project_name]** as its base (`basename` for react-router, `base` for vue-router, etc.): ```javascript - + diff --git a/docs/custom-functions/requirements-definitions.md b/docs/custom-functions/requirements-definitions.md index 2a66cce6..d789f8d9 100644 --- a/docs/custom-functions/requirements-definitions.md +++ b/docs/custom-functions/requirements-definitions.md @@ -1,8 +1,5 @@ # Requirements And Definitions -Before you get started with Custom Functions, here’s a primer on Custom Functions’ basic configuration and the structure of a Custom Functions Project. - -## Availability -Custom Functions is available in HarperDB version 3.1.0 and later. +Before you get started with Custom Functions, here’s a primer on the basic configuration and the structure of a Custom Functions Project. ## Configuration Custom Functions is enabled by default. If you wish to manage Custom Functions directly, you will find the relevant configuration parameters in your [configuration file](https://harperdb.io/docs/reference/configuration-file/). The available settings are listed below: @@ -16,7 +13,7 @@ Custom Functions is enabled by default. If you wish to manage Custom Functions d This is the port HarperDB will use to start a standalone Fastify Server dedicated to serving your Custom Functions’ routes. * **customFunctions.root** - This is the root directory where your Custom Functions projects and their files will live. By default, it’s in your ~/hdb folder, but you can locate it anywhere- in a Developer folder next to your other development projects, for example. + This is the root directory where your Custom Functions projects and their files will live. By default, it’s in your , but you can locate it anywhere--in a developer folder next to your other development projects, for example. * **customFunctions.processes** The number of processes you wish to spin up for the Custom Functions Fastify server. @@ -24,7 +21,7 @@ Custom Functions is enabled by default. If you wish to manage Custom Functions d ## Project Structure **project folder** -The name of the folder that holds your project files serves as the root prefix for all the routes you create. All routes created in the **dogs** project folder will have a URL like this: **https://my-server-url.com:9926/dogs/my/route**. As such, it’s important that any project folders you create avoid any characters that aren’t URL-friendly. If you stick to lowercase letters and hyphens, you should be fine. +The name of the folder that holds your project files serves as the root prefix for all the routes you create. All routes created in the **dogs** project folder will have a URL like this: **https://my-server-url.com:9926/dogs/my/route**. As such, it’s important that any project folders you create avoid any characters that aren’t URL-friendly. You should avoid URL delimiters in your folder names. **/routes folder** @@ -44,7 +41,7 @@ module.exports = async (server, { hdbCore, logger }) => { **/helpers folder** -These files are plain javascript functions that you can use in your handlers, or for custom `preValidation` hooks. Examples include calls to third party Authentication services, filters for results of calls to HarperDB, and custom error responses. You must export them as a **module**, and you import them using **require**. +These files are JavaScript modules that you can use in your handlers, or for custom `preValidation` hooks. Examples include calls to third party Authentication services, filters for results of calls to HarperDB, and custom error responses. As modules, you can use standard import and export functionality. ```javascript "use strict"; From dab4f6e31ef911af57592106f3e293f8c780915a Mon Sep 17 00:00:00 2001 From: terraHDB Date: Tue, 25 Oct 2022 15:52:35 -0500 Subject: [PATCH 20/33] corrections to cf doc migration --- docs/custom-functions/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/custom-functions/index.md b/docs/custom-functions/index.md index aee6d649..2a90f1f9 100644 --- a/docs/custom-functions/index.md +++ b/docs/custom-functions/index.md @@ -1,5 +1,7 @@ # Custom Functions +Custom functions are a key part of building a complete HarperDB application. It is highly recommended that you use custom functions as the primary mechanism for your application to access your HarperDB database. Using custom functions gives you complete control over the accessible endpoints, how users are authenticated and authorized, what data is accessed from the database, and how it is aggregated and returned to users. + * Add your own API endpoints to a standalone API server inside HarperDB * Use HarperDB Core methods to interact with your data at lightning speed From 5252844f3b7f72c81a3e86cb6e3331b559f31903 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Thu, 27 Oct 2022 10:51:13 -0500 Subject: [PATCH 21/33] updated request and prevalidation helpers in accordance to pr comments --- docs/custom-functions/define-routes.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/custom-functions/define-routes.md b/docs/custom-functions/define-routes.md index b6ed6fcc..635fd58f 100644 --- a/docs/custom-functions/define-routes.md +++ b/docs/custom-functions/define-routes.md @@ -102,15 +102,13 @@ hdbCore contains three functions that allow you to authenticate an inbound reque * **request** - Executes a request against HarperDB. It calls the operations API, with authorization. + This will execute a request with HarperDB using the operations API. The `request.body` should contain a standard HarperDB operation and must also include the `hdb_user` property that was in `request.body` provided in the callback. * **requestWithoutAuthentication** Executes a request against HarperDB without any security checks around whether the inbound user is allowed to make this request. For security purposes, you should always take the following precautions when using this method: - * **never pass through a user-submitted operation.** Always define your operation in your handler. A user could send an operation like drop_schema, and hdbCore.requestWithoutAuthentication will let them do it. - - * **never use user-submitted values in your operations, including url params.** Always validate user input to ensure nothing malformed or malicious becomes part of the operation you send to hdbCore.requestWithoutAuthentication. + * Properly handle user-submitted values, including url params. User-submitted values should only be used for `search_value` and for defining values in records. Special care should be taken to properly escape any values if user-submitted values are used for SQL. **logger** From b05d4fa993b9dcf0fec484ac9ce9f17fb0f59568 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Fri, 11 Nov 2022 09:49:16 -0700 Subject: [PATCH 22/33] addressed comments on cf docs --- docs/custom-functions/create-project.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/custom-functions/create-project.md b/docs/custom-functions/create-project.md index 493ebea1..33e40b1a 100644 --- a/docs/custom-functions/create-project.md +++ b/docs/custom-functions/create-project.md @@ -18,6 +18,8 @@ To manually create a project, you have three options: 2. **clone our public gitHub project template** + + _This requires a local installation. Remove the .git directory for a clean slate of git history._ ```bash @@ -27,6 +29,9 @@ To manually create a project, you have three options: 3. **create a project folder in your Custom Functions root directory** + _This requires a local installation._ + + ```bash > mkdir ~/hdb/custom_functions/dogs ``` \ No newline at end of file From de141877475ff3e348adf7eb4f451be373b49a99 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Tue, 15 Nov 2022 10:27:35 -0600 Subject: [PATCH 23/33] cleaned up function with fetch --- docs/custom-functions/define-helpers.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/custom-functions/define-helpers.md b/docs/custom-functions/define-helpers.md index f88a0633..89f8404a 100644 --- a/docs/custom-functions/define-helpers.md +++ b/docs/custom-functions/define-helpers.md @@ -37,16 +37,10 @@ const authRequest = (options) => { }; const customValidation = async (request,logger) => { - const options = { - hostname: 'jsonplaceholder.typicode.com', - port: 443, - path: '/todos/1', - method: 'GET', - headers: { authorization: request.headers.authorization }, - }; - - const result = await authRequest(options); + let response = await fetch('https://jsonplaceholder.typicode.com/todos/1', { headers: { authorization: request.headers.authorization } }); + let result = await response.json(); + /* * throw an authentication error based on the response body or statusCode */ From b40c80459b84000f796f5c7337219eee47f0deb5 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Tue, 15 Nov 2022 12:03:37 -0600 Subject: [PATCH 24/33] reverted changes to function --- docs/custom-functions/define-helpers.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/custom-functions/define-helpers.md b/docs/custom-functions/define-helpers.md index 89f8404a..a078026f 100644 --- a/docs/custom-functions/define-helpers.md +++ b/docs/custom-functions/define-helpers.md @@ -37,9 +37,15 @@ const authRequest = (options) => { }; const customValidation = async (request,logger) => { - - let response = await fetch('https://jsonplaceholder.typicode.com/todos/1', { headers: { authorization: request.headers.authorization } }); - let result = await response.json(); + const options = { + hostname: 'jsonplaceholder.typicode.com', + port: 443, + path: '/todos/1', + method: 'GET', + headers: { authorization: request.headers.authorization }, + }; + + const result = await authRequest(options); /* * throw an authentication error based on the response body or statusCode From 81c76304560200e7476a4d445ad868b75716e7a7 Mon Sep 17 00:00:00 2001 From: terraHDB <93155880+terraHDB@users.noreply.github.com> Date: Tue, 15 Nov 2022 14:20:38 -0600 Subject: [PATCH 25/33] Core 1828 Docs Migration- Security: Topic and Subtopics (#4) * doc migration epic branch * 3/4 progress of security * finished configuration.md, began users and roles * users and roles migrated; still needs updates * updated user and roles operations * corrections to security docs * CORE-1825 Add/Update "install harpedb" subtopics (#1) * added mac, added/updated windows, added/updated node version requirements, and added offline docs * go requirements * docs adjustments * testing variable usage * testing variable usage * Update other.md Update installation for other operating systems. * added link to download * node version * exact replica download button * functional download link * deleted old link * node version Co-authored-by: Kris Zyp * Core 1826 add/update harperdb studio topic and subtopics (#2) * added mac, added/updated windows, added/updated node version requirements, and added offline docs * 1/3 through studio subtopics * 2/3 through studio subtopics * 3/3 through studio subtopics * corrected harperdb operations api * removed icon descriptions and fixed typos * removed icon descriptions * primary key vs hash_attribute change * primary key vs hash attribute qualifier * minor change to example function Co-authored-by: Kris Zyp --- docs/harperdb-cloud/index.md | 1 + docs/harperdb-studio/create-account.md | 22 ++ docs/harperdb-studio/enable-mixed-content.md | 7 + docs/harperdb-studio/index.md | 10 + .../harperdb-studio/instance-configuration.md | 115 ++++++++ docs/harperdb-studio/instance-example-code.md | 58 ++++ docs/harperdb-studio/instance-metrics.md | 15 + docs/harperdb-studio/instances.md | 137 +++++++++ docs/harperdb-studio/login-password-reset.md | 38 +++ docs/harperdb-studio/manage-charts.md | 75 +++++ docs/harperdb-studio/manage-clustering.md | 90 ++++++ docs/harperdb-studio/manage-functions.md | 167 +++++++++++ docs/harperdb-studio/manage-instance-roles.md | 72 +++++ docs/harperdb-studio/manage-instance-users.md | 59 ++++ .../manage-schemas-browse-data.md | 128 ++++++++ docs/harperdb-studio/organizations.md | 101 +++++++ docs/harperdb-studio/query-instance-data.md | 49 +++ docs/harperdb-studio/resources.md | 39 +++ docs/install-harperdb/index.md | 7 +- docs/install-harperdb/mac.md | 17 ++ docs/install-harperdb/node-ver-requirement.md | 7 + docs/install-harperdb/offline.md | 14 + docs/install-harperdb/other.md | 9 + docs/install-harperdb/windows.md | 13 + docs/security/basic-auth.md | 64 ++++ docs/security/configuration.md | 57 ++++ docs/security/index.md | 8 + docs/security/jwt-auth.md | 93 ++++++ docs/security/users-and-roles.md | 278 ++++++++++++++++++ images/ave-age-per-owner-ex.png | Bin 0 -> 38173 bytes images/xcode.png | Bin 0 -> 50262 bytes 31 files changed, 1747 insertions(+), 3 deletions(-) create mode 100644 docs/harperdb-studio/create-account.md create mode 100644 docs/harperdb-studio/enable-mixed-content.md create mode 100644 docs/harperdb-studio/instance-configuration.md create mode 100644 docs/harperdb-studio/instance-example-code.md create mode 100644 docs/harperdb-studio/instance-metrics.md create mode 100644 docs/harperdb-studio/instances.md create mode 100644 docs/harperdb-studio/login-password-reset.md create mode 100644 docs/harperdb-studio/manage-charts.md create mode 100644 docs/harperdb-studio/manage-clustering.md create mode 100644 docs/harperdb-studio/manage-functions.md create mode 100644 docs/harperdb-studio/manage-instance-roles.md create mode 100644 docs/harperdb-studio/manage-instance-users.md create mode 100644 docs/harperdb-studio/manage-schemas-browse-data.md create mode 100644 docs/harperdb-studio/organizations.md create mode 100644 docs/harperdb-studio/query-instance-data.md create mode 100644 docs/harperdb-studio/resources.md create mode 100644 docs/install-harperdb/mac.md create mode 100644 docs/install-harperdb/node-ver-requirement.md create mode 100644 docs/install-harperdb/offline.md create mode 100644 docs/install-harperdb/other.md create mode 100644 docs/install-harperdb/windows.md create mode 100644 docs/security/basic-auth.md create mode 100644 docs/security/configuration.md create mode 100644 docs/security/jwt-auth.md create mode 100644 docs/security/users-and-roles.md create mode 100644 images/ave-age-per-owner-ex.png create mode 100644 images/xcode.png diff --git a/docs/harperdb-cloud/index.md b/docs/harperdb-cloud/index.md index 8f125a56..cfa4d3ca 100644 --- a/docs/harperdb-cloud/index.md +++ b/docs/harperdb-cloud/index.md @@ -1 +1,2 @@ # HarperDB Cloud + diff --git a/docs/harperdb-studio/create-account.md b/docs/harperdb-studio/create-account.md new file mode 100644 index 00000000..b39a2a03 --- /dev/null +++ b/docs/harperdb-studio/create-account.md @@ -0,0 +1,22 @@ +# Create a Studio Account +Start at the [HarperDB Studio sign up page](https://studio.harperdb.io/sign-up). + +1) Provide the following information: + * First Name + * Last Name + * Email Address + * Subdomain + + *Part of the URL that will be used to identify your HarperDB Cloud Instances. For example, with subdomain “demo” and instance name “c1” the instance URL would be: https://c1-demo.harperdbcloud.com.* + * Coupon Code (optional) +2) Review the Privacy Policy and Terms of Service. +3) Click the sign up for free button. +4) You will be taken to a new screen to add an account password. Enter your password. + *Passwords must be a minimum of 8 characters with at least 1 lower case character, 1 upper case character, 1 number, and 1 special character.* +5) Click the add account password button. + +You will receive a Studio welcome email confirming your registration. + + + +Note: Your email address will be used as your username and cannot be changed. \ No newline at end of file diff --git a/docs/harperdb-studio/enable-mixed-content.md b/docs/harperdb-studio/enable-mixed-content.md new file mode 100644 index 00000000..1c2e9674 --- /dev/null +++ b/docs/harperdb-studio/enable-mixed-content.md @@ -0,0 +1,7 @@ +# Enable Mixed Content + +Enabling mixed content is required in cases where you would like to connect the HarperDB Studio to HarperDB Instances via HTTP. This should not be used for production systems, but may be convenient for development and testing purposes. Doing so will allow your browser to reach HTTP traffic, which is considered insecure, through an HTTPS site like the Studio. + + + +A comprehensive guide is provided by Adobe here: https://experienceleague.adobe.com/docs/target/using/experiences/vec/troubleshoot-composer/mixed-content.html. \ No newline at end of file diff --git a/docs/harperdb-studio/index.md b/docs/harperdb-studio/index.md index a90590dc..6db82fe6 100644 --- a/docs/harperdb-studio/index.md +++ b/docs/harperdb-studio/index.md @@ -1 +1,11 @@ # HarperDB Studio +HarperDB Studio is the web-based GUI for HarperDB. Studio enables you to administer, navigate, and monitor all of your HarperDB instances in a simple, user friendly interface without any knowledge of the underlying HarperDB API. It’s free to sign up, get started today! + +[Sign up for free!](https://studio.harperdb.io/sign-up) + +--- +## How does Studio Work? +While HarperDB Studio is web based and hosted by us, all database interactions are performed locally. The HarperDB Studio loads in your browser, at which point you login to your HarperDB instances. Credentials are stored in your browser cache and are not transmitted back to HarperDB. All database interactions are made via the HarperDB Operations API directly from your browser to your instance. + +## What type of instances can I manage? +HarperDB Studio enables users to manage both HarperDB Cloud instances and privately hosted instances all from a single UI. All HarperDB instances feature identical behavior whether they are hosted by us or by you. \ No newline at end of file diff --git a/docs/harperdb-studio/instance-configuration.md b/docs/harperdb-studio/instance-configuration.md new file mode 100644 index 00000000..de88c7f0 --- /dev/null +++ b/docs/harperdb-studio/instance-configuration.md @@ -0,0 +1,115 @@ +# Instance Configuration + +HarperDB instance configuration can be viewed and managed directly through the HarperDB Studio. HarperDB Cloud instances can be resized in two different ways via this page, either by modifying machine RAM or by increasing drive storage. User-installed instances can have their licenses modified by modifying licensed RAM. + + + +All instance configuration is handled through the **config** page of the HarperDB Studio, accessed with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click config in the instance control bar. + +*Note, the **config** page will only be available to super users and certain items are restricted to Studio organization owners.* + +## Instance Overview + +The **instance overview** panel displays the following instance specifications: + +* Instance URL + +* Instance Node Name (for clustering) + +* Instance API Auth Header (this user) + + *The Basic authentication header used for the logged in HarperDB database user* + +* Created Date (HarperDB Cloud only) + +* Region (HarperDB Cloud only) + + *The geographic region where the instance is hosted.* + +* Total Price + +* RAM + +* Storage (HarperDB Cloud only) + +* Disk IOPS (HarperDB Cloud only) + +## Update Instance RAM + +HarperDB Cloud instance size and user-installed instance licenses can be modified with the following instructions. This option is only available to Studio organization owners. + + + +Note: For HarperDB Cloud instances, upgrading RAM may add additional CPUs to your instance as well. Click here to see how many CPUs are provisioned for each instance size. + +1) In the **update ram** panel at the bottom left: + + * Select the new instance size. + + * If you do not have a credit card associated with your account, an **Add Credit Card To Account** button will appear. Click that to be taken to the billing screen where you can enter your credit card information before returning to the **config** tab to proceed with the upgrade. + + * If you do have a credit card associated, you will be presented with the updated billing information. + + * Click **Upgrade**. + +2) The instance will shut down and begin reprovisioning/relicensing itself. The instance will not be available during this time. You will be returned to the instance dashboard and the instance status will show UPDATING INSTANCE. + +3) Once your instance upgrade is complete, it will appear on the instance dashboard as status OK with your newly selected instance size. + +*Note, if HarperDB Cloud instance reprovisioning takes longer than 20 minutes, please submit a support ticket here: https://harperdbhelp.zendesk.com/hc/en-us/requests/new.* + +## Update Instance Storage + +The HarperDB Cloud instance storage size can be increased with the following instructions. This option is only available to Studio organization owners. + +Note: Instance storage can only be upgraded once every 6 hours. + +1) In the **update storage** panel at the bottom left: + + * Select the new instance storage size. + + * If you do not have a credit card associated with your account, an **Add Credit Card To Account** button will appear. Click that to be taken to the billing screen where you can enter your credit card information before returning to the **config** tab to proceed with the upgrade. + + * If you do have a credit card associated, you will be presented with the updated billing information. + + * Click **Upgrade**. + +2) The instance will shut down and begin reprovisioning itself. The instance will not be available during this time. You will be returned to the instance dashboard and the instance status will show UPDATING INSTANCE. + +3) Once your instance upgrade is complete, it will appear on the instance dashboard as status OK with your newly selected instance size. + +*Note, if this process takes longer than 20 minutes, please submit a support ticket here: https://harperdbhelp.zendesk.com/hc/en-us/requests/new.* + +## Remove Instance + +The HarperDB instance can be deleted/removed from the Studio with the following instructions. Once this operation is started it cannot be undone. This option is only available to Studio organization owners. + +1) In the **remove instance** panel at the bottom left: + * Enter the instance name in the text box. + + * The Studio will present you with a warning. + + * Click **Remove**. + +2) The instance will begin deleting immediately. + +## Restart Instance + +The HarperDB Cloud instance can be restarted with the following instructions. + +1) In the **restart instance** panel at the bottom right: + * Enter the instance name in the text box. + + * The Studio will present you with a warning. + + * Click **Restart**. + +2) The instance will begin restarting immediately. \ No newline at end of file diff --git a/docs/harperdb-studio/instance-example-code.md b/docs/harperdb-studio/instance-example-code.md new file mode 100644 index 00000000..e942f07f --- /dev/null +++ b/docs/harperdb-studio/instance-example-code.md @@ -0,0 +1,58 @@ +# Instance Example Code + +Example code prepopulated with the instance URL and authorization token for the logged in database user can be found on the **example code** page of the HarperDB Studio. Code samples are generated based on the HarperDB API Documentation Postman collection. Code samples accessed with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click **example code** in the instance control bar. + +5) Select the appropriate **category** from the left navigation. + +6) Select the appropriate **operation** from the left navigation. + +7) Select your desired language/variant from the **Choose Programming Language** dropdown. + +8) Copy code from the sample code panel using the copy icon. + +## Supported Languages + +Sample code uses two identifiers: **language** and **variant**. + +* **language** is the programming language that the sample code is generated in. + +* **variant** is the methodology or library used by the language to send HarperDB requests. + +The list of available language/variants are as follows: + +| Language | Variant | +|--------------|---------------| +| C# | RestSharp | +| cURL | cURL | +| Go | Native | +| HTTP | HTTP | +| Java | OkHttp | +| Java | Unirest | +| JavaScript | Fetch | +| JavaScript | jQuery | +| JavaScript | XHR | +| NodeJs | Axios | +| NodeJs | Native | +| NodeJs | Request | +| NodeJs | Unirest | +| Objective-C | NSURLSession | +| OCaml | Cohttp | +| PHP | cURL | +| PHP | HTTP_Request2 | +| PowerShell | RestMethod | +| Python | http.client | +| Python | Requests | +| Ruby | Net:HTTP | +| Shell | Httpie | +| Shell | wget | +| Swift | URLSession | + + diff --git a/docs/harperdb-studio/instance-metrics.md b/docs/harperdb-studio/instance-metrics.md new file mode 100644 index 00000000..2dcd80c7 --- /dev/null +++ b/docs/harperdb-studio/instance-metrics.md @@ -0,0 +1,15 @@ +# Instance Metrics + +The HarperDB Studio display instance status and metrics on the instance status page, which can be accessed with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click **status** in the instance control bar. + +Once on the instance browse page you can view host system information, [HarperDB logs](https://harperdb.io/docs/reference/logging/), [HarperDB jobs](https://harperdb.io/docs/reference/job-logs/), and [HarperDB Cloud alarms](https://harperdb.io/docs/harperdb-cloud/alarms/) (if it is a cloud instance). + +*Note, the **status** page will only be available to super users.* \ No newline at end of file diff --git a/docs/harperdb-studio/instances.md b/docs/harperdb-studio/instances.md new file mode 100644 index 00000000..a5180e69 --- /dev/null +++ b/docs/harperdb-studio/instances.md @@ -0,0 +1,137 @@ +# Instances + +The HarperDB Studio allows you to administer all of your HarperDB instances in one place. HarperDB currently offers the following instance types: + +* **HarperDB Cloud Instance** +Managed installations of HarperDB, what we call [HarperDB Cloud](https://harperdb.io/docs/harperdb-cloud/). +* **5G Wavelength Instance** +Managed installations of HarperDB running on the Verizon network through AWS Wavelength, what we call [5G Wavelength Instances](https://harperdb.io/docs/harperdb-cloud/verizon-5g-wavelength/). *Note, these instances are only accessible via the Verizon network.* +* **User-Installed Instance** +Any HarperDB installation that is managed by you. These include instances hosted within your cloud provider accounts (for example, from the AWS or Digital Ocean Marketplaces), privately hosted instances, or instances installed locally. + +All interactions between the Studio and your instances take place directly from your browser. HarperDB stores metadata about your instances, which enables the Studio to display these instances when you login. Beyond that, all traffic is routed from your browser to the HarperDB instances using the standard [HarperDB API](https://harperdb.io/docs/harperdb-api/). + +## Organization Instance List +A summary view of all instances within an organization can be viewed by clicking on the appropriate organization from the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. Each instance gets their own card. HarperDB Cloud and user-installed instances are listed together. + +## Create a New Instance +A new instance can be created as follows: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. +2) Click the appropriate organization for the instance to be created under. +3) Click the **Create New HarperDB Cloud Instance + Register User-Installed Instance** card. +4) Select your desired Instance Type. +5) For a HarperDB Cloud Instance or a HarperDB 5G Wavelength Instance, click **Create HarperDB Cloud Instance**. + + 1) Fill out Instance Info. + 1) Enter Instance Name + + *This will be used to build your instance URL. For example, with subdomain “demo” and instance name “c1” the instance URL would be: https://c1-demo.harperdbcloud.com. The Instance URL will be previewed below.* + + 2) Enter Instance Username + + *This is the username of the initial HarperDB instance super user.* + + 3) Enter Instance Password + + *This is the password of the initial HarperDB instance super user.* + + 2) Click **Instance Details** to move to the next page. + 3) Select Instance Specs + + 1) Select Instance RAM + + *HarperDB Cloud Instances are billed based on Instance RAM, this will select the size of your provisioned instance. [More on instance specs](https://harperdb.io/docs/harperdb-cloud/instance-size-hardware-specs/).* + + 2) Select Storage Size + + *Each instance has a mounted storage volume where your HarperDB data will reside. Storage is provisioned based on space and IOPS. [More on IOPS Impact on Performance](https://harperdb.io/docs/harperdb-cloud/iops-impact-on-performance/).* + + 3) Select Instance Region + + *The geographic area where your instance will be provisioned.* + + 4) Click **Confirm Instance Details** to move to the next page. + 5) Review your Instance Details, if there is an error, use the back button to correct it. + 6) Review the [Privacy Policy](https://harperdb.io/legal/privacy-policy/) and [Terms of Service](https://harperdb.io/legal/harperdb-cloud-terms-of-service/), if you agree, click the **I agree** radio button to confirm. + 7) Click **Add Instance**. + 8) Your HarperDB Cloud instance will be provisioned in the background. Provisioning typically takes 5-15 minutes. You will receive an email notification when your instance is ready. + +6) For a user-installed instance, click **Register User-Installed Instance**. + + 1) Fill out Instance Info. + + 1) Enter Instance Name + + *This is used for descriptive purposes only.* + 2) Enter Instance Username + + *The username of a HarperDB super user that is already configured in your HarperDB installation.* + 3) Enter Instance Password + + *The password of a HarperDB super user that is already configured in your HarperDB installation.* + 4) Enter Host + + *The host to access the HarperDB instance. For example, `harperdb.myhost.com` or `localhost`.* + 5) Enter Port + + *The port to access the HarperDB instance. HarperDB defaults `9925` for HTTP and `31283` for HTTPS.* + 6) Select SSL + + *If your instance is running over SSL, select the SSL checkbox. If not, you will need to enable mixed content in your browser to allow the HTTPS Studio to access the HTTP instance. If there are issues connecting to the instance, the Studio will display a red error message.* + + 2) Click **Instance Details** to move to the next page. + 3) Select Instance Specs + 1) Select Instance RAM + + *HarperDB instances are billed based on Instance RAM. Selecting additional RAM will enable the ability for faster and more complex queries.* + 4) Click **Confirm Instance Details** to move to the next page. + 5) Review your Instance Details, if there is an error, use the back button to correct it. + 6) Review the [Privacy Policy](https://harperdb.io/legal/privacy-policy/) and [Terms of Service](https://harperdb.io/legal/harperdb-cloud-terms-of-service/), if you agree, click the **I agree** radio button to confirm. + 7) Click **Add Instance**. + 8) The HarperDB Studio will register your instance and restart it for the registration to take effect. Your instance will be immediately available after this is complete. + +## Delete an Instance + +Instance deletion has two different behaviors depending on the instance type. + +* **HarperDB Cloud Instance** +This instance will be permanently deleted, including all data. This process is irreversible and cannot be undone. +* **User-Installed Instance** +The instance will be removed from the HarperDB Studio only. This does not uninstall HarperDB from your system and your data will remain intact. + +An instance can be deleted as follows: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. +2) Click the appropriate organization that the instance belongs to. +3) Identify the proper instance card and click the trash can icon. +4) Enter the instance name into the text box. + + *This is done for confirmation purposes to ensure you do not accidentally delete an instance.* +5) Click the **Do It** button. + +## Upgrade an Instance + +HarperDB instances can be resized on the [Instance Configuration](https://harperdb.io/docs/harperdb-studio/instance-config/) page. + +## Instance Log In/Log Out + +The Studio enables users to log in and out of different database users from the instance control panel. To log out of an instance: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. +2) Click the appropriate organization that the instance belongs to. +3) Identify the proper instance card and click the lock icon. +4) You will immediately be logged out of the instance. + +To log in to an instance: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. +2) Click the appropriate organization that the instance belongs to. +3) Identify the proper instance card, it will have an unlocked icon and a status reading PLEASE LOG IN, and click the center of the card. +4) Enter the database username. + + *The username of a HarperDB user that is already configured in your HarperDB instance.* +5) Enter the database password. + + *The password of a HarperDB user that is already configured in your HarperDB instance.* +6) Click **Log In**. \ No newline at end of file diff --git a/docs/harperdb-studio/login-password-reset.md b/docs/harperdb-studio/login-password-reset.md new file mode 100644 index 00000000..99cca0a6 --- /dev/null +++ b/docs/harperdb-studio/login-password-reset.md @@ -0,0 +1,38 @@ +# Login and Password Reset + +## Log In to Your HarperDB Studio Account + +To log into your existing HarperDB Studio account: + +1) Navigate to the [HarperDB Studio](https://studio.harperdb.io/). +2) Enter your email address. +3) Enter your password. +4) Click **sign in**. + +## Reset a Forgotten Password + +To reset a forgotten password: + +1) Navigate to the HarperDB Studio password reset page. +2) Enter your email address. +3) Click **send password reset email**. +4) If the account exists, you will receive an email with a temporary password. +5) Navigate back to the HarperDB Studio login page. +6) Enter your email address. +7) Enter your temporary password. +8) Click **sign in**. +9) You will be taken to a new screen to reset your account password. Enter your new password. +*Passwords must be a minimum of 8 characters with at least 1 lower case character, 1 upper case character, 1 number, and 1 special character.* +10) Click the **add account password** button. + +## Change Your Password + +If you are already logged into the Studio, you can change your password though the user interface. + +1) Navigate to the HarperDB Studio profile page. +2) In the **password** section, enter: + + * Current password. + * New password. + * New password again *(for verification)*. +4) Click the **Update Password** button. \ No newline at end of file diff --git a/docs/harperdb-studio/manage-charts.md b/docs/harperdb-studio/manage-charts.md new file mode 100644 index 00000000..5f45ac9a --- /dev/null +++ b/docs/harperdb-studio/manage-charts.md @@ -0,0 +1,75 @@ +# Charts + +The HarperDB Studio includes a charting feature within an instance. They are generated in real time based on your existing data and automatically refreshed every 15 seconds. Instance charts can be accessed with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. +2) Click the appropriate organization that the instance belongs to. +3) Select your desired instance. +4) Click **charts** in the instance control bar. + +## Creating a New Chart + +Charts are generated based on SQL queries, therefore to build a new chart you first need to build a query. Instructions as follows (starting on the charts page described above): + +1) Click **query** in the instance control bar. +2) Enter the SQL query you would like to generate a chart from. + + *For example, using the dog demo data from the API Docs, we can get the average dog age per owner with the following query: `SELECT AVG(age) as avg_age, owner_name FROM dev.dog GROUP BY owner_name`.* + +3) Click **Execute**. + +4) Click **create chart** at the top right of the results table. + +5) Configure your chart. + + 1) Choose chart type. + + *HarperDB Studio offers many standard charting options like line, bar, etc.* + + 2) Choose a data column. + + *This column will be used to plot the data point. Typically, this is the values being calculated in the `SELECT` statement. Depending on the chart type, you can select multiple data columns to display on a single chart.* + 3) Depending on the chart type, you will need to select a grouping. + + *This could be labeled as x-axis, label, etc. This will be used to group the data, typically this is what you used in your **GROUP BY** clause.* + + 4) Enter a chart name. + + *Used for identification purposes and will be displayed at the top of the chart.* + + 5) Choose visible to all org users toggle. + + *Leaving this option off will limit chart visibility to just your HarperDB Studio user. Toggling it on will enable all users with this Organization to view this chart.* + + 6) Click **Add Chart**. + + 7) The chart will now be visible on the **charts** page. + +The example query above, configured as a bar chart, results in the following chart: + +![Average Age per Owner Example](../../images/ave-age-per-owner-ex.png) + + +## Downloading Charts +HarperDB Studio charts can be downloaded in SVG, PNG, and CSV format. Instructions as follows (starting on the charts page described above): + +1) Identify the chart you would like to export. +2) Click the three bars icon. + +3) Select the appropriate download option. + +4) The Studio will generate the export and begin downloading immediately. + +## Delete a Chart + +Delete a chart as follows (starting on the charts page described above): + +1) Identify the chart you would like to delete. + +2) Click the X icon. + +3) Click the **confirm delete chart** button. + +4) The chart will be deleted. + +Deleting a chart that is visible to all Organization users will delete it for all users. \ No newline at end of file diff --git a/docs/harperdb-studio/manage-clustering.md b/docs/harperdb-studio/manage-clustering.md new file mode 100644 index 00000000..a3c70ba9 --- /dev/null +++ b/docs/harperdb-studio/manage-clustering.md @@ -0,0 +1,90 @@ +# Manage Clustering + +HarperDB instance clustering and replication can be configured directly through the HarperDB Studio. It is recommended to read through the clustering documentation first to gain a strong understanding of HarperDB clustering behavior. + + + +All clustering configuration is handled through the **cluster** page of the HarperDB Studio, accessed with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click **cluster** in the instance control bar. + +Note, the **cluster** page will only be available to super users. + +--- +## Initial Configuration + +HarperDB instances do not have clustering configured by default. The HarperDB Studio will walk you through the initial configuration. Upon entering the **cluster** screen for the first time you will need to complete the following configuration. Configurations are set in the **enable clustering** panel on the left while actions are described in the middle of the screen. + +1) Create a cluster user, read more about this here: Clustering Users and Roles. + * Enter username. + + * Enter password. + + * Click **Create Cluster User**. + +2) Click **Set Cluster Node Name**. +3) Click **Enable Instance Clustering**. + +At this point the Studio will restart your HarperDB Instance, required for the configuration changes to take effect. + +--- + +## Manage Clustering +Once initial clustering configuration is completed you a presented with a clustering management screen with the following properties: + +* **connected instances** + + Displays all instances within the Studio Organization that this instance manages a connection with. + +* **unconnected instances** + + Displays all instances within the Studio Organization that this instance does not manage a connection with. + +* **unregistered instances** + + Displays all instances outside of the Studio Organization that this instance manages a connection with. + +* **manage clustering** + + Once instances are connected, this will display clustering management options for all connected instances and all schemas and tables. +--- + +## Connect an Instance + +HarperDB Instances can be clustered together with the following instructions. + +1) Ensure clustering has been configured on both instances and a cluster user with identical credentials exists on both. + +2) Identify the instance you would like to connect from the **unconnected instances** panel. + +3) Click the plus icon next the appropriate instance. + +4) If configurations are correct, all schemas will sync across the cluster, then appear in the **manage clustering** panel. If there is a configuration issue, a red exclamation icon will appear, click it to learn more about what could be causing the issue. + +--- + +## Disconnect an Instance + +HarperDB Instances can be disconnected with the following instructions. + +1) Identify the instance you would like to disconnect from the **connected instances** panel. + +2) Click the minus icon next the appropriate instance. + +--- + +## Manage Replication + +Subscriptions must be configured in order to move data between connected instances. Read more about subscriptions here: Creating A Subscription. The **manage clustering** panel displays a table with each row representing an channel per instance. Cells are bolded to indicate a change in the column. Publish and subscribe replication can be configured per table with the following instructions: + +1) Identify the instance, schema, and table for replication to be configured. + +2) For publish, click the toggle switch in the **publish** column. + +3) For subscribe, click the toggle switch in the **subscribe** column. \ No newline at end of file diff --git a/docs/harperdb-studio/manage-functions.md b/docs/harperdb-studio/manage-functions.md new file mode 100644 index 00000000..499337f0 --- /dev/null +++ b/docs/harperdb-studio/manage-functions.md @@ -0,0 +1,167 @@ +# Manage Functions + +HarperDB Custom Functions can be configured directly through the HarperDB Studio. It is recommended to read through the Custom Functions documentation first to gain a strong understanding of HarperDB Custom Functions behavior. + + + +All Custom Functions configuration is handled through the **functions** page of the HarperDB Studio, accessed with the following instructions: + +1) Navigate to the HarperDB Studio Organizations page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click **functions** in the instance control bar. + +*Note, the **functions** page will only be available to super users.* + +## Initial Configuration + +HarperDB instances do not have Custom Functions configured by default. The HarperDB Studio will walk you through the initial configuration. Upon entering the **functions** screen for the first time you will need to complete the following configuration. Configurations are set in the **enable custom functions** panel on the left while actions are described in the middle of the screen. + +1) Click **Enable Custom Functions**. + +At this point the Studio will restart your HarperDB Instance, required for the configuration changes to take effect. + +## Manage Projects + +Once initial Custom Functions configuration is completed you a presented with a functions management screen with the following properties: + +* **projects** + + Displays a list of Custom Functions projects residing on this instance. +* **/project_name/routes** + + Only displayed if there is an existing project. Displays the routes files contained within the selected project. +* **/project_name/helpers** + + Only displayed if there is an existing project. Displays the helper files contained within the selected project. +* **/project_name/static** + + Only displayed if there is an existing project. Displays the static file count and a link to the static files contained within the selected project. Note, static files cannot currently be deployed through the Studio and must be deployed via the [HarperDB API](https://harperdb.io/docs/harperdb-api/) or manually to the server (not applicable with HarperDB Cloud). +* **Root File Directory** + + Displays the root file directory where the Custom Functions projects reside on this instance. +* **Custom Functions Server URL** + + Displays the base URL in which all Custom Functions are accessed for this instance. + + +## Create a Project + +HarperDB Custom Functions Projects can be initialized with the following instructions. + +1) If this is your first project, skip this step. Click the plus icon next to the **projects** heading. + +2) Enter the project name in the text box located under the **projects** heading. + +3) Click the check mark icon next the appropriate instance. + +4) The Studio will take a few moments to provision a new project based on the [Custom Functions template](https://github.com/HarperDB/harperdb-custom-functions-template). + +5) The Custom Functions project is now created and ready to modify. + +## Modify a Project + +Custom Functions routes and helper functions can be modified directly through the Studio. From the **functions** page: + +1) Select the appropriate **project**. + +2) Select the appropriate **route** or **helper**. + +3) Modify the code with your desired changes. + +4) Click the save icon at the bottom right of the screen. + + *Note, saving modifications will restart the Custom Functions server on your HarperDB instance and may result in up to 60 seconds of downtime for all Custom Functions.* + +## Create Additional Routes/Helpers + +To create an additional **route** to your Custom Functions project. From the **functions** page: + +1) Select the appropriate Custom Functions **project**. + +2) Click the plus icon to the right of the **routes** header. + +3) Enter the name of the new route in the textbox that appears. + +4) Click the check icon to create the new route. + + *Note, adding a route will restart the Custom Functions server on your HarperDB instance and may result in up to 60 seconds of downtime for all Custom Functions.* + +To create an additional **helper** to your Custom Functions project. From the **functions** page: + +1) Select the appropriate Custom Functions **project**. + +2) Click the plus icon to the right of the **helpers** header. + +3) Enter the name of the new helper in the textbox that appears. + +4) Click the check icon to create the new helper. + + *Note, adding a helper will restart the Custom Functions server on your HarperDB instance and may result in up to 60 seconds of downtime for all Custom Functions.* + +## Delete a Project/Route/Helper + +To delete a Custom Functions project from the **functions** page: + +1) Click the minus icon to the right of the **projects** header. + +2) Click the red minus icon to the right of the Custom Functions project you would like to delete. + +3) Confirm deletion by clicking the red check icon. + + *Note, deleting a project will restart the Custom Functions server on your HarperDB instance and may result in up to 60 seconds of downtime for all Custom Functions.* + +To delete a Custom Functions _project route_ from the **functions** page: + +1) Select the appropriate Custom Functions **project**. + +2) Click the minus icon to the right of the **routes** header. + +3) Click the red minus icon to the right of the Custom Functions route you would like to delete. + +4) Confirm deletion by clicking the red check icon. + + *Note, deleting a route will restart the Custom Functions server on your HarperDB instance and may result in up to 60 seconds of downtime for all Custom Functions.* + +To delete a Custom Functions _project helper_ from the **functions** page: + +1) Select the appropriate Custom Functions **project**. + +2) Click the minus icon to the right of the **helper** header. + +3) Click the red minus icon to the right of the Custom Functions header you would like to delete. + +4) Confirm deletion by clicking the red check icon. + + *Note, deleting a header will restart the Custom Functions server on your HarperDB instance and may result in up to 60 seconds of downtime for all Custom Functions.* + +## Deploy Custom Functions Project to Other Instances + +The HarperDB Studio provides the ability to deploy Custom Functions projects to additional HarperDB instances within the same Studio Organization. To deploy Custom Functions projects to additional instances, starting from the **functions** page: + +1) Select the **project** you would like to deploy. + +2) Click the **deploy** button at the top right. + +3) A list of instances (excluding the current instance) within the organization will be displayed in tabular with the following information: + + * **Instance Name**: The name used to describe the instance. + + * **Instance URL**: The URL used to access the instance. + + * **CF Capable**: Describes if the instance version supports Custom Functions (yes/no). + + * **CF Enabled**: Describes if Custom Functions are configured and enabled on the instance (yes/no). + + * **Has Project**: Describes if the selected Custom Functions project has been previously deployed to the instance (yes/no). + + * **Deploy**: Button used to deploy the project to the instance. + + * **Remote**: Button used to remove the project from the instance. *Note, this will only be visible if the project has been previously deployed to the instance.* + +4) In the appropriate instance row, click the **deploy** button. + + *Note, deploying a project will restart the Custom Functions server on the HarperDB instance receiving the deployment and may result in up to 60 seconds of downtime for all Custom Functions.* \ No newline at end of file diff --git a/docs/harperdb-studio/manage-instance-roles.md b/docs/harperdb-studio/manage-instance-roles.md new file mode 100644 index 00000000..967489b7 --- /dev/null +++ b/docs/harperdb-studio/manage-instance-roles.md @@ -0,0 +1,72 @@ +# Manage Instance Roles + +HarperDB users can be managed directly through the HarperDB Studio. It is recommended to read through the users & roles documentation to gain a strong understanding of how they operate. + + + +Instance role configuration is handled through the roles page of the HarperDB Studio, accessed with the following instructions: + +1) Navigate to the HarperDB Studio Organizations page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click **rules** in the instance control bar. + +*Note, the **roles** page will only be available to super users.* + + + +The *roles management* screen consists of the following panels: + +* **super users** + + Displays all super user roles for this instance. +* **cluster users** + + Displays all cluster user roles for this instance. +* **standard roles** + + Displays all standard roles for this instance. +* **role permission editing** + + Once a role is selected for editing, permissions will be displayed here in JSON format. + +*Note, when new tables are added that are not configured, the Studio will generate configuration values with permissions defaulting to `false`.* + +## Role Management + +#### Create a Role + +1) Click the plus icon at the top right of the appropriate role section. + +2) Enter the role name. + +3) Click the green check mark. + +4) Configure the role permissions in the role permission editing panel. + + *Note, to have the Studio generate attribute permissions JSON, toggle **show all attributes** at the top right of the role permission editing panel.* + +5) Click **Update Role Permissions**. + +#### Modify a Role + +1) Click the appropriate role from the appropriate role section. + +2) Modify the role permissions in the role permission editing panel. + + *Note, to have the Studio generate attribute permissions JSON, toggle **show all attributes** at the top right of the role permission editing panel.* + +3) Click **Update Role Permissions**. + +#### Delete a Role + +Deleting a role is permanent and irreversible. A role cannot be remove if users are associated with it. + +1) Click the minus icon at the top right of the schemas section. + +2) Identify the appropriate role to delete and click the red minus sign in the same row. + +3) Click the red check mark to confirm deletion. \ No newline at end of file diff --git a/docs/harperdb-studio/manage-instance-users.md b/docs/harperdb-studio/manage-instance-users.md new file mode 100644 index 00000000..1b638d69 --- /dev/null +++ b/docs/harperdb-studio/manage-instance-users.md @@ -0,0 +1,59 @@ +# Manage Instance Users + +HarperDB instance clustering and replication can be configured directly through the HarperDB Studio. It is recommended to read through the clustering documentation first to gain a strong understanding of HarperDB clustering behavior. + + + +Instance user configuration is handled through the **users** page of the HarperDB Studio, accessed with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. + +2) Click the appropriate organization that the instance belongs to. + +3) Select your desired instance. + +4) Click **users** in the instance control bar. + +*Note, the **users** page will only be available to super users.* + +## Add a User + +HarperDB instance users can be added with the following instructions. + +1) In the **add user** panel on the left enter: + + * New user username. + + * New user password. + + * Select a role. + + *Learn more about role management here: [Manage Instance Roles](https://harperdb.io/docs/harperdb-studio/manage-instance-roles/).* + +2) Click **Add User**. + +## Edit a User + +HarperDB instance users can be modified with the following instructions. + +1) In the **existing users** panel, click the row of the user you would like to edit. + +2) To change a user’s password: + + 1) In the **Change user password** section, enter the new password. + + 2) Click **Update Password**. + +3) To change a user’s role: + + 1) In the **Change user role** section, select the new role. + + 2) Click **Update Role**. + +4) To delete a user: + + 1) In the **Delete User** section, type the username into the textbox. + + *This is done for confirmation purposes.* + + 2) Click **Delete User**. \ No newline at end of file diff --git a/docs/harperdb-studio/manage-schemas-browse-data.md b/docs/harperdb-studio/manage-schemas-browse-data.md new file mode 100644 index 00000000..0f5f3d9b --- /dev/null +++ b/docs/harperdb-studio/manage-schemas-browse-data.md @@ -0,0 +1,128 @@ +# Manage Schemas / Browse Data + +Manage instance schemas/tables and browse data in tabular format with the following instructions: + +1) Navigate to the HarperDB Studio Organizations page. +2) Click the appropriate organization that the instance belongs to. +3) Select your desired instance. +4) Click **browse** in the instance control bar. + +Once on the instance browse page you can view data, manage schemas and tables, add new data, and more. + +## Manage Schemas and Tables + +#### Create a Schema + +1) Click the plus icon at the top right of the schemas section. +2) Enter the schema name. +3) Click the green check mark. + + +#### Delete a Schema + +Deleting a schema is permanent and irreversible. Deleting a schema removes all tables and data within it. + +1) Click the minus icon at the top right of the schemas section. +2) Identify the appropriate schema to delete and click the red minus sign in the same row. +3) Click the red check mark to confirm deletion. + + +#### Create a Table + +1) Select the desired schema from the schemas section. +2) Click the plus icon at the top right of the tables section. +3) Enter the table name. +4) Enter the primary key. + + *The primary key is also often referred to as the hash attribute in the studio, and it defines the unique identifier for each row in your table.* +5) Click the green check mark. + + +#### Delete a Table +Deleting a table is permanent and irreversible. Deleting a table removes all data within it. + +1) Select the desired schema from the schemas section. +2) Click the minus icon at the top right of the tables section. +3) Identify the appropriate table to delete and click the red minus sign in the same row. +4) Click the red check mark to confirm deletion. + +## Manage Table Data + +The following section assumes you have selected the appropriate table from the schema/table browser. + + + +#### Filter Table Data + +1) Click the magnifying glass icon at the top right of the table browser. +2) This expands the search filters. +3) The results will be filtered appropriately. + + +#### Load CSV Data + +1) Click the data icon at the top right of the table browser. You will be directed to the CSV upload page where you can choose to import a CSV by URL or upload a CSV file. +2) To import a CSV by URL: + 1) Enter the URL in the **CSV file URL** textbox. + 2) Click **Import From URL**. + 3) The CSV will load and you will be redirected back to browse table data. +3) To upload a CSV file: + 1) Click **Click or Drag to select a .csv file** (or drag your CSV file from your file browser). + 2) Navigate to your desired CSV file and select it. + 3) Click **Insert X Records**, where X is the number of records in your CSV. + 4) The CSV will load, and you will be redirected back to browse table data. + + +#### Add a Record + +1) Click the plus icon at the top right of the table browser. +2) The Studio will pre-populate existing table attributes in JSON format. + + *The primary key is not included, but you can add it in and set it to your desired value. Auto-maintained fields are not included and cannot be manually set. You may enter a JSON array to insert multiple records in a single transaction.* +3) Enter values to be added to the record. + + *You may add new attributes to the JSON; they will be reflexively added to the table.* +4) Click the **Add New** button. + + +#### Edit a Record + +1) Click the record/row you would like to edit. +2) Modify the desired values. + + *You may add new attributes to the JSON; they will be reflexively added to the table.* + +3) Click the **save icon**. + + +#### Delete a Record + +Deleting a record is permanent and irreversible. If transaction logging is turned on, the delete transaction will be recorded as well as the data that was deleted. + +1) Click the record/row you would like to delete. +2) Click the **delete icon**. +3) Confirm deletion by clicking the **check icon**. + +## Browse Table Data + +The following section assumes you have selected the appropriate table from the schema/table browser. + +#### Browse Table Data + +The first page of table data is automatically loaded on table selection. Paging controls are at the bottom of the table. Here you can: + +* Page left and right using the arrows. +* Type in the desired page. +* Change the page size (the amount of records displayed in the table). + + +#### Refresh Table Data + +Click the refresh icon at the top right of the table browser. + + + +#### Automatically Refresh Table Data + +Toggle the auto switch at the top right of the table browser. The table data will now automatically refresh every 15 seconds. Filters and pages will remain set for refreshed data. + diff --git a/docs/harperdb-studio/organizations.md b/docs/harperdb-studio/organizations.md new file mode 100644 index 00000000..8206601f --- /dev/null +++ b/docs/harperdb-studio/organizations.md @@ -0,0 +1,101 @@ +# Organizations +HarperDB Studio organizations provide the ability to group HarperDB Cloud Instances. Organization behavior is as follows: + +* Billing occurs at the organization level to a single credit card. +* Organizations retain their own unique HarperDB Cloud subdomain. +* Cloud instances reside within an organization. +* Studio users can be invited to organizations to share instances. + + +An organization is automatically created for you when you sign up for HarperDB Studio. If you only have one organization, the Studio will automatically bring you to your organization’s page. + +--- + +## List Organizations +A summary view of all organizations your user belongs to can be viewed on the [HarperDB Studio Organizations](https://studio.harperdb.io/?redirect=/organizations) page. You can navigate to this page at any time by clicking the **all organizations** link at the top of the HarperDB Studio. + +## Create a New Organization +A new organization can be created as follows: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/?redirect=/organizations) page. +2) Click the **Create a New Organization** card. +3) Fill out new organization details + * Enter Organization Name + *This is used for descriptive purposes only.* + * Enter Organization Subdomain + *Part of the URL that will be used to identify your HarperDB Cloud Instances. For example, with subdomain “demo” and instance name “c1” the instance URL would be: https://c1-demo.harperdbcloud.com.* +4) Click Create Organization. + +## Delete an Organization +An organization cannot be deleted until all instances have been removed. An organization can be deleted as follows: + +1) Navigate to the HarperDB Studio Organizations page. +2) Identify the proper organization card and click the trash can icon. +3) Enter the organization name into the text box. + + *This is done for confirmation purposes to ensure you do not accidentally delete an organization.* +4) Click the **Do It** button. + +## Manage Users +HarperDB Studio organization owners can manage users including inviting new users, removing users, and toggling ownership. + + + +#### Inviting a User +A new user can be invited to an organization as follows: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/?redirect=/organizations) page. +2) Click the appropriate organization card. +3) Click **users** at the top of the screen. +4) In the **add user** box, enter the new user’s email address. +5) Click **Add User**. + +Users may or may not already be HarperDB Studio users when adding them to an organization. If the HarperDB Studio account already exists, the user will receive an email notification alerting them to the organization invitation. If the user does not have a HarperDB Studio account, they will receive an email welcoming them to HarperDB Studio. + +--- + +#### Toggle a User’s Organization Owner Status +Organization owners have full access to the organization including the ability to manage organization users, create, modify, and delete instances, and delete the organization. Users must have accepted their invitation prior to being promoted to an owner. A user’s organization owner status can be toggled owner as follows: + +1) Navigate to the HarperDB Studio Organizations page. +2) Click the appropriate organization card. +3) Click **users** at the top of the screen. +4) Click the appropriate user from the **existing users** section. +5) Toggle the **Is Owner** switch to the desired status. +--- + +#### Remove a User from an Organization +Users may be removed from an organization at any time. Removing a user from an organization will not delete their HarperDB Studio account, it will only remove their access to the specified organization. A user can be removed from an organization as follows: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/?redirect=/organizations) page. +2) Click the appropriate organization card. +3) Click **users** at the top of the screen. +4) Click the appropriate user from the **existing users** section. +5) Type **DELETE** in the text box in the **Delete User** row. + + *This is done for confirmation purposes to ensure you do not accidentally delete a user.* +6) Click **Delete User**. + +## Manage Billing + +Billing is configured per organization and will be billed to the stored credit card at appropriate intervals (monthly or annually depending on the registered instance). Billing settings can be configured as follows: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/?redirect=/organizations) page. +2) Click the appropriate organization card. +3) Click **billing** at the top of the screen. + +Here organization owners can view invoices, manage coupons, and manage the associated credit card. + + + +*HarperDB billing and payments are managed via Stripe.* + + + +### Add a Coupon + +Coupons are applicable towards any paid tier or user-installed instance and you can change your subscription at any time. Coupons can be added to your Organization as follows: + +1) In the coupons panel of the **billing** page, enter your coupon code. +2) Click **Add Coupon**. +3) The coupon will then be available and displayed in the coupons panel. \ No newline at end of file diff --git a/docs/harperdb-studio/query-instance-data.md b/docs/harperdb-studio/query-instance-data.md new file mode 100644 index 00000000..04f178b8 --- /dev/null +++ b/docs/harperdb-studio/query-instance-data.md @@ -0,0 +1,49 @@ +# Query Instance Data + +SQL queries can be executed directly through the HarperDB Studio with the following instructions: + +1) Navigate to the [HarperDB Studio Organizations](https://studio.harperdb.io/organizations) page. +2) Click the appropriate organization that the instance belongs to. +3) Select your desired instance. +4) Click **query** in the instance control bar. +5) Enter your SQL query in the SQL query window. +6) Click **Execute**. + +*Please note, the Studio will execute the query exactly as entered. For example, if you attempt to `SELECT *` from a table with millions of rows, you will most likely crash your browser.* + +## Browse Query Results Set + +#### Browse Results Set Data + +The first page of results set data is automatically loaded on query execution. Paging controls are at the bottom of the table. Here you can: + +* Page left and right using the arrows. +* Type in the desired page. +* Change the page size (the amount of records displayed in the table). + +#### Refresh Results Set + +Click the refresh icon at the top right of the results set table. + +#### Automatically Refresh Results Set + +Toggle the auto switch at the top right of the results set table. The results set will now automatically refresh every 15 seconds. Filters and pages will remain set for refreshed data. + +## Query History + +Query history is stored in your local browser cache. Executed queries are listed with the most recent at the top in the **query history** section. + + +#### Rerun Previous Query + +* Identify the query from the **query history** list. +* Click the appropriate query. It will be loaded into the **sql query** input box. +* Click **Execute**. + +#### Clear Query History + +Click the trash can icon at the top right of the **query history** section. + +## Create Charts + +The HarperDB Studio includes a charting feature where you can build charts based on your specified queries. Visit the Charts documentation for more information. \ No newline at end of file diff --git a/docs/harperdb-studio/resources.md b/docs/harperdb-studio/resources.md new file mode 100644 index 00000000..7269e57f --- /dev/null +++ b/docs/harperdb-studio/resources.md @@ -0,0 +1,39 @@ +# Resources (Marketplace, Drivers, Tutorials, & Example Code) + +HarperDB Studio resources are available regardless of whether or not you are logged in. + +# HarperDB Marketplace + +The [HarperDB Marketplace](https://studio.harperdb.io/resources/marketplace/active) is a collection of SDKs and connectors that enable developers to expand upon HarperDB for quick and easy solution development. Extensions are built and supported by the HarperDB Community. Each extension is hosted on the appropriate package manager or host. + + + +To download a Marketplace extension: + +1) Navigate to the [HarperDB Marketplace](https://studio.harperdb.io/resources/marketplace/active) page. +2) Identity the extension you would like to use. +3) Either click the link to the package. +4) Follow the extension’s instructions to proceed. + +You can submit your rating for each extension by clicking on the stars. + +## HarperDB Drivers + +HarperDB offers standard drivers to connect real-time HarperDB data with BI, analytics, reporting and data visualization technologies. Drivers are built and maintained by [CData Software](https://www.cdata.com/drivers/harperdb/). + + + +To download a driver: + +1) Navigate to the [HarperDB Drivers](https://studio.harperdb.io/resources/marketplace/active) page. +2) Identity the driver you would like to use. +3) Click the download link. +4) For additional instructions, visit the support link on the driver card. + +## Video Tutorials + +HarperDB offers video tutorials available in the Studio on the [HarperDB Tutorials](https://studio.harperdb.io/resources/tutorials/UExsZ1RNVEtzeXBTNUdJbjRZaTNOeEM0aW5YX3RBNU85SS4yODlGNEE0NkRGMEEzMEQy) page as well as our [YouTube channel](https://www.youtube.com/playlist?list=PLlgTMTKsypS5GIn4Yi3NxC4inX_tA5O9I). The HarperDB Studio is changing all the time, as a result these, the videos may not include all of the current Studio features. + +## Example Code + +The [code examples](https://studio.harperdb.io/resources/examples/QuickStart%20Examples/Create%20dev%20Schema) page offers example code for many different programming languages. These samples will include a placeholder for your authorization token. Full code examples with the authorization token prepopulated are available within individual instance pages. \ No newline at end of file diff --git a/docs/install-harperdb/index.md b/docs/install-harperdb/index.md index 1c8841fc..84de45de 100644 --- a/docs/install-harperdb/index.md +++ b/docs/install-harperdb/index.md @@ -6,6 +6,7 @@ This section contains technical details and reference materials for installing H * [Linux](linux.md) * [Docker](docker.md) -* Mac -* Windows (WSL2) -* Offline +* [Mac](mac.md) +* [Windows](windows.md) +* [Offline](offline.md) +* [Other](other.md) \ No newline at end of file diff --git a/docs/install-harperdb/mac.md b/docs/install-harperdb/mac.md new file mode 100644 index 00000000..e4f97bea --- /dev/null +++ b/docs/install-harperdb/mac.md @@ -0,0 +1,17 @@ +# Mac Development Requirements + +MacOS is not recommended as a production operating system, but it is great for development. For more information on standard HarperDB installation visit the HarperDB Command Line Interface guide. This page details the unique cases you may run into when installing on Mac. + + + +Please note, HarperDB suggests Node.js v18.12.0 as a prerequisite to installation, but we allow Node.js versions 14.0.0 & higher. [Learn more here](https://harperdb.io/docs/install-harperdb/node-version/). + +--- + +To install HarperDB please visit the npm install guide: https://www.npmjs.com/package/harperdb. + +or from the command line: +```bash +npm install -g harperdb +harperdb +``` diff --git a/docs/install-harperdb/node-ver-requirement.md b/docs/install-harperdb/node-ver-requirement.md new file mode 100644 index 00000000..ae4e6e0b --- /dev/null +++ b/docs/install-harperdb/node-ver-requirement.md @@ -0,0 +1,7 @@ +# Node Version Requirement + +HarperDB installations suggest Node.js v18.12.0, but we allow Node.js versions 14.0.0 & higher, in order to be installed. We recommend using Node Version Manager (nvm) to manage your Node.js installations, an example of how to install HarperDB using nvm can be found on our Linux Install doc. + + + +HarperDB 4.0.0, our current release, suggests Node.js v18.12.0. \ No newline at end of file diff --git a/docs/install-harperdb/offline.md b/docs/install-harperdb/offline.md new file mode 100644 index 00000000..b1ff72de --- /dev/null +++ b/docs/install-harperdb/offline.md @@ -0,0 +1,14 @@ +# Offline Install + +If you need to install HarperDB on a device that doesn't have an Internet connection, you can download the npm package and install it directly (you’ll still need Node.js and NPM): + +Download Install Package + + +Once you’ve downloaded the .tgz file, run the following command from the directory where you’ve placed it: + +```bash +npm install -g harperdb-X.X.X.tgz harperdb install +``` + +For more information visit the [HarperDB Command Line Interface](https://harperdb.io/docs/administration/harperdb-cli/) guide. \ No newline at end of file diff --git a/docs/install-harperdb/other.md b/docs/install-harperdb/other.md new file mode 100644 index 00000000..9fc7a500 --- /dev/null +++ b/docs/install-harperdb/other.md @@ -0,0 +1,9 @@ +# Other Install Information + + +If you are installing on a platform other than linux-amd64, linux-arm64, darwin-amd64, darwin-arm64, windows-amd64, you will need to ensure that you have installed: +* [Go](https://go.dev/dl/): version 1.19.1 +* GCC +* Make +* Python v3.7, v3.8, v3.9, or v3.10 + diff --git a/docs/install-harperdb/windows.md b/docs/install-harperdb/windows.md new file mode 100644 index 00000000..d9713b1e --- /dev/null +++ b/docs/install-harperdb/windows.md @@ -0,0 +1,13 @@ +# Install HarperDB on Windows + +Running HarperDB on Windows 10 or 11 is a great way to test and develop with HarperDB. We do not, though, recommend HarperDB on Windows for production workloads. + +--- + +Install HarperDB on Windows + +```bash +npm install -g harperdb +harperdb +```` +HarperDB will automatically start after installation. For more information visit the [HarperDB Command Line Interface guide](https://harperdb.io/docs/administration/harperdb-cli/). diff --git a/docs/security/basic-auth.md b/docs/security/basic-auth.md new file mode 100644 index 00000000..9cf0678b --- /dev/null +++ b/docs/security/basic-auth.md @@ -0,0 +1,64 @@ +# Authentication + +HarperDB uses Basic Auth and JSON Web Tokens (JWTs) to secure our HTTP requests. In the context of an HTTP transaction, **basic access authentication** is a method for an HTTP user agent to provide a user name and password when making a request. + + + +** ***You do not need to log in separately. Basic Auth is added to each HTTP request like create_schema, create_table, insert etc… via headers.*** ** + + + +A header is added to each HTTP request. The header key is **“Authorization”** the header value is **“Basic <>”** + + + + + +## Authentication in HarperDB Studio + +Below is a code sample from HarperDB Studio. On Line 14 you will see where we are adding the authorization header to each request. This needs to be added for each and every HTTP request for HarperDB. + +```javascript +function callHarperDB(call_object, operation, callback){ + + const options = { + "method": "POST", + "hostname": call_object.endpoint_url, + "port": call_object.endpoint_port, + "path": "/", + "headers": { + "content-type": "application/json", + "authorization": "Basic " + btoa(call_object.username + ':' + call_object.password), + "cache-control": "no-cache" + + } + }; + + const http_req = http.request(options, function (hdb_res) { + let chunks = []; + + hdb_res.on("data", function (chunk) { + chunks.push(chunk); + }); + + hdb_res.on("end", function () { + const body = Buffer.concat(chunks); + if (isJson(body)) { + return callback(null, JSON.parse(body)); + } else { + return callback(body, null); + + } + + }); + }); + + http_req.on("error", function (chunk) { + return callback("Failed to connect", null); + }); + + http_req.write(JSON.stringify(operation)); + http_req.end(); + +} +``` \ No newline at end of file diff --git a/docs/security/configuration.md b/docs/security/configuration.md new file mode 100644 index 00000000..708cd573 --- /dev/null +++ b/docs/security/configuration.md @@ -0,0 +1,57 @@ +# Configuration + +HarperDB was set up to require very minimal configuration to work out of the box. There are, however, some best practices we encourage for anyone building an app with HarperDB. + + + +## CORS + +HarperDB allows for managing [cross-origin HTTP requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). By default, HarperDB enables CORS for all domains if you need to disable CORS completely or set up an access list of domains you can do the following: + +1) Open the harperdb-config.yaml file this can be found in /, the location you specified during install. + +2) In harperdb-config.yaml there should be 2 entries under `operationsApi.network`: cors and corsAccessList. + * `cors` + + 1) To turn off, change to: `cors: false` + + 2) To turn on, change to: `cors: true` + + * `corsAccessList` + + 1) The `corsAccessList` will only be recognized by the system when `cors` is `true` + + 2) To create an access list you set `corsAccessList` to a comma-separated list of domains. + + i.e. `corsAccessList` is `http://harperdb.io,http://products.harperdb.io` + + 3) To clear out the access list and allow all domains: `corsAccessList` is `[null]` + + +## SSL + +HarperDB provides the option to use an HTTP or HTTPS interface. The default port for the server is 9925. + + + +These default ports can be changed by updating the `operationsApi.network.port` value in `/harperdb-config.yaml` + + + +By default, HTTPS is turned off and HTTP is turned on. It is recommended that you never directly expose HarperDB's HTTP interface through a publicly available port. HTTP is intended for local or private network use. + + + +You can toggle HTTPS and HTTP in the settings file. By setting `operationsApi.network.https` to true/false. When `https` is set to `false`, the server will use HTTP. + + + +HarperDB automatically generates a certificate and a privateKey file which live at `/keys/`. + + + +You can replace these with your own certificate and key. + + + +**If any of these settings are changed please make sure to run `harperdb restart` as they will not take effect until a restart.** \ No newline at end of file diff --git a/docs/security/index.md b/docs/security/index.md index 8dbb2f9b..9bba60da 100644 --- a/docs/security/index.md +++ b/docs/security/index.md @@ -1 +1,9 @@ # Security + +HarperDB uses role-based, attribute-level security to ensure that users can only gain access to the data they’re supposed to be able to access. Our granular permissions allow for unparalleled flexibility and control, and can actually lower the total cost of ownership compared to other database solutions, since you no longer have to replicate subsets of your data to isolate use cases. + +* [JWT Authentication](https://harperdb.io/docs/security/jwt-authentication/) +* [Basic Authentication](https://harperdb.io/docs/security/authentication/) +* [Configuration](https://harperdb.io/docs/security/configuration/) +* [Users and Roles](https://harperdb.io/docs/security/users-roles/) + diff --git a/docs/security/jwt-auth.md b/docs/security/jwt-auth.md new file mode 100644 index 00000000..a7ff19ea --- /dev/null +++ b/docs/security/jwt-auth.md @@ -0,0 +1,93 @@ +# JWT Authentication +HarperDB uses token based authentication with JSON Web Tokens, JWTs. + +This consists of two primary operations `create_authentication_tokens` and `refresh_operation_token`. These generate two types of tokens, as follows: + +* The `operation_token` which is used to authenticate all HarperDB operations in the Bearer Token Authorization Header. The default expiry is one day. + +* The `refresh_token` which is used to generate a new `operation_token` upon expiry. This token is used in the Bearer Token Authorization Header for the `refresh_operation_token` operation only. The default expiry is thirty days. + +The `create_authentication_tokens` operation can be used at any time to refresh both tokens in the event that both have expired or been lost. + +## Create Authentication Tokens + +Users must initially create tokens using their HarperDB credentials. The following POST body is sent to HarperDB. No headers are required for this POST operation. + +```json +{ + "operation": "create_authentication_tokens", + "username": "username", + "password": "password" +} +``` + +A full cURL example can be seen here: + +```bash +curl --location --request POST 'http://localhost:9925' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "operation": "create_authentication_tokens", + "username": "username", + "password": "password" +}' +``` + +An example expected return object is: + +```json +{ + "operation_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNjA0OTc4MjAwLCJleHAiOjE2MDUwNjQ2MDAsInN1YiI6Im9wZXJhdGlvbiJ9.MpQA-9CMjA-mn-7mHyUXSuSC_-kqMqJXp_NDiKLFtbtMRbodCuY3DzH401rvy_4vb0yCELf0B5EapLVY1545sv80nxSl6FoZFxQaDWYXycoia6zHpiveR8hKlmA6_XTWHJbY2FM1HAFrdtt3yUTiF-ylkdNbPG7u7fRjTmHfsZ78gd2MNWIDkHoqWuFxIyqk8XydQpsjULf2Uacirt9FmHfkMZ-Jr_rRpcIEW0FZyLInbm6uxLfseFt87wA0TbZ0ofImjAuaW_3mYs-3H48CxP152UJ0jByPb0kHsk1QKP7YHWx1-Wce9NgNADfG5rfgMHANL85zvkv8sJmIGZIoSpMuU3CIqD2rgYnMY-L5dQN1fgfROrPMuAtlYCRK7r-IpjvMDQtRmCiNG45nGsM4DTzsa5GyDrkGssd5OBhl9gr9z9Bb5HQVYhSKIOiy72dK5dQNBklD4eGLMmo-u322zBITmE0lKaBcwYGJw2mmkYcrjDOmsDseU6Bf_zVUd9WF3FqwNkhg4D7nrfNSC_flalkxPHckU5EC_79cqoUIX2ogufBW5XgYbU4WfLloKcIpb51YTZlZfwBHlHPSyaq_guaXFaeCUXKq39_i1n0HRF_mRaxNru0cNDFT9Fm3eD7V8axFijSVAMDyQs_JR7SY483YDKUfN4l-vw-EVynImr4", + "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNjA0OTc4MjAwLCJleHAiOjE2MDc1NzAyMDAsInN1YiI6InJlZnJlc2gifQ.acaCsk-CJWIMLGDZdGnsthyZsJfQ8ihXLyE8mTji8PgGkpbwhs7e1O0uitMgP_pGjHq2tey1BHSwoeCL49b18WyMIB10hK-q2BXGKQkykltjTrQbg7VsdFi0h57mGfO0IqAwYd55_hzHZNnyJMh4b0iPQFDwU7iTD7x9doHhZAvzElpkWbc_NKVw5_Mw3znjntSzbuPN105zlp4Niurin-_5BnukwvoJWLEJ-ZlF6hE4wKhaMB1pWTJjMvJQJE8khTTvlUN8tGxmzoaDYoe1aCGNxmDEQnx8Y5gKzVd89sylhqi54d2nQrJ2-ElfEDsMoXpR01Ps6fNDFtLTuPTp7ixj8LvgL2nCjAg996Ga3PtdvXJAZPDYCqqvaBkZZcsiqOgqLV0vGo3VVlfrcgJXQImMYRr_Inu0FCe47A93IAWuQTs-KplM1KdGJsHSnNBV6oe6QEkROJT5qZME-8xhvBYvOXqp9Znwg39bmiBCMxk26Ce66_vw06MNgoa3D5AlXPWemfdVKPZDnj_aLVjZSs0gAfFElcVn7l9yjWJOaT2Muk26U8bJl-2BEq_DSclqKHODuYM5kkPKIdE4NFrsqsDYuGxcA25rlNETFyl0q-UXj1aoz_joy5Hdnr4mFELmjnoo4jYQuakufP9xeGPsj1skaodKl0mmoGcCD6v1F60" +} +``` + +## Using JWT Authentication Tokens + +The `operation_token` value is used to authenticate all operations in place of our standard Basic auth. In order to pass the token you will need to create an Bearer Token Authorization Header like the following request: + +```bash +curl --location --request POST 'http://localhost:9925' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNjA0OTc4MjAwLCJleHAiOjE2MDUwNjQ2MDAsInN1YiI6Im9wZXJhdGlvbiJ9.MpQA-9CMjA-mn-7mHyUXSuSC_-kqMqJXp_NDiKLFtbtMRbodCuY3DzH401rvy_4vb0yCELf0B5EapLVY1545sv80nxSl6FoZFxQaDWYXycoia6zHpiveR8hKlmA6_XTWHJbY2FM1HAFrdtt3yUTiF-ylkdNbPG7u7fRjTmHfsZ78gd2MNWIDkHoqWuFxIyqk8XydQpsjULf2Uacirt9FmHfkMZ-Jr_rRpcIEW0FZyLInbm6uxLfseFt87wA0TbZ0ofImjAuaW_3mYs-3H48CxP152UJ0jByPb0kHsk1QKP7YHWx1-Wce9NgNADfG5rfgMHANL85zvkv8sJmIGZIoSpMuU3CIqD2rgYnMY-L5dQN1fgfROrPMuAtlYCRK7r-IpjvMDQtRmCiNG45nGsM4DTzsa5GyDrkGssd5OBhl9gr9z9Bb5HQVYhSKIOiy72dK5dQNBklD4eGLMmo-u322zBITmE0lKaBcwYGJw2mmkYcrjDOmsDseU6Bf_zVUd9WF3FqwNkhg4D7nrfNSC_flalkxPHckU5EC_79cqoUIX2ogufBW5XgYbU4WfLloKcIpb51YTZlZfwBHlHPSyaq_guaXFaeCUXKq39_i1n0HRF_mRaxNru0cNDFT9Fm3eD7V8axFijSVAMDyQs_JR7SY483YDKUfN4l-vw-EVynImr4' \ +--data-raw '{ + "operation":"search_by_hash", + "schema":"dev", + "table":"dog", + "hash_values":[1], + "get_attributes": ["*"] +}' +``` + +## Token Expiration + +`operation_token` expires at a set interval. Once it expires it will no longer be accepted by HarperDB. This duration defaults to one day, and is configurable in [harperdb-config.yaml](https://harperdb.io/docs/reference/configuration-file/). To generate a new `operation_token`, the `refresh_operation_token` operation is used, passing the `refresh_token` in the Bearer Token Authorization Header. A full cURL example can be seen here: + +```bash +curl --location --request POST 'http://localhost:9925' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNjA0OTc4MjAwLCJleHAiOjE2MDc1NzAyMDAsInN1YiI6InJlZnJlc2gifQ.acaCsk-CJWIMLGDZdGnsthyZsJfQ8ihXLyE8mTji8PgGkpbwhs7e1O0uitMgP_pGjHq2tey1BHSwoeCL49b18WyMIB10hK-q2BXGKQkykltjTrQbg7VsdFi0h57mGfO0IqAwYd55_hzHZNnyJMh4b0iPQFDwU7iTD7x9doHhZAvzElpkWbc_NKVw5_Mw3znjntSzbuPN105zlp4Niurin-_5BnukwvoJWLEJ-ZlF6hE4wKhaMB1pWTJjMvJQJE8khTTvlUN8tGxmzoaDYoe1aCGNxmDEQnx8Y5gKzVd89sylhqi54d2nQrJ2-ElfEDsMoXpR01Ps6fNDFtLTuPTp7ixj8LvgL2nCjAg996Ga3PtdvXJAZPDYCqqvaBkZZcsiqOgqLV0vGo3VVlfrcgJXQImMYRr_Inu0FCe47A93IAWuQTs-KplM1KdGJsHSnNBV6oe6QEkROJT5qZME-8xhvBYvOXqp9Znwg39bmiBCMxk26Ce66_vw06MNgoa3D5AlXPWemfdVKPZDnj_aLVjZSs0gAfFElcVn7l9yjWJOaT2Muk26U8bJl-2BEq_DSclqKHODuYM5kkPKIdE4NFrsqsDYuGxcA25rlNETFyl0q-UXj1aoz_joy5Hdnr4mFELmjnoo4jYQuakufP9xeGPsj1skaodKl0mmoGcCD6v1F60' \ +--data-raw '{ + "operation":"refresh_operation_token" +}' +``` + +This will return a new `operation_token`. An example expected return object is: + +```bash +{ + "operation_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6eyJfX2NyZWF0ZWR0aW1lX18iOjE2MDQ5NzgxODkxNTEsIl9fdXBkYXRlZHRpbWVfXyI6MTYwNDk3ODE4OTE1MSwiYWN0aXZlIjp0cnVlLCJyb2xlIjp7Il9fY3JlYXRlZHRpbWVfXyI6MTYwNDk0NDE1MTM0NywiX191cGRhdGVkdGltZV9fIjoxNjA0OTQ0MTUxMzQ3LCJpZCI6IjdiNDNlNzM1LTkzYzctNDQzYi05NGY3LWQwMzY3Njg5NDc4YSIsInBlcm1pc3Npb24iOnsic3VwZXJfdXNlciI6dHJ1ZSwic3lzdGVtIjp7InRhYmxlcyI6eyJoZGJfdGFibGUiOnsicmVhZCI6dHJ1ZSwiaW5zZXJ0IjpmYWxzZSwidXBkYXRlIjpmYWxzZSwiZGVsZXRlIjpmYWxzZSwiYXR0cmlidXRlX3Blcm1pc3Npb25zIjpbXX0sImhkYl9hdHRyaWJ1dGUiOnsicmVhZCI6dHJ1ZSwiaW5zZXJ0IjpmYWxzZSwidXBkYXRlIjpmYWxzZSwiZGVsZXRlIjpmYWxzZSwiYXR0cmlidXRlX3Blcm1pc3Npb25zIjpbXX0sImhkYl9zY2hlbWEiOnsicmVhZCI6dHJ1ZSwiaW5zZXJ0IjpmYWxzZSwidXBkYXRlIjpmYWxzZSwiZGVsZXRlIjpmYWxzZSwiYXR0cmlidXRlX3Blcm1pc3Npb25zIjpbXX0sImhkYl91c2VyIjp7InJlYWQiOnRydWUsImluc2VydCI6ZmFsc2UsInVwZGF0ZSI6ZmFsc2UsImRlbGV0ZSI6ZmFsc2UsImF0dHJpYnV0ZV9wZXJtaXNzaW9ucyI6W119LCJoZGJfcm9sZSI6eyJyZWFkIjp0cnVlLCJpbnNlcnQiOmZhbHNlLCJ1cGRhdGUiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJhdHRyaWJ1dGVfcGVybWlzc2lvbnMiOltdfSwiaGRiX2pvYiI6eyJyZWFkIjp0cnVlLCJpbnNlcnQiOmZhbHNlLCJ1cGRhdGUiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJhdHRyaWJ1dGVfcGVybWlzc2lvbnMiOltdfSwiaGRiX2xpY2Vuc2UiOnsicmVhZCI6dHJ1ZSwiaW5zZXJ0IjpmYWxzZSwidXBkYXRlIjpmYWxzZSwiZGVsZXRlIjpmYWxzZSwiYXR0cmlidXRlX3Blcm1pc3Npb25zIjpbXX0sImhkYl9pbmZvIjp7InJlYWQiOnRydWUsImluc2VydCI6ZmFsc2UsInVwZGF0ZSI6ZmFsc2UsImRlbGV0ZSI6ZmFsc2UsImF0dHJpYnV0ZV9wZXJtaXNzaW9ucyI6W119LCJoZGJfbm9kZXMiOnsicmVhZCI6dHJ1ZSwiaW5zZXJ0IjpmYWxzZSwidXBkYXRlIjpmYWxzZSwiZGVsZXRlIjpmYWxzZSwiYXR0cmlidXRlX3Blcm1pc3Npb25zIjpbXX0sImhkYl90ZW1wIjp7InJlYWQiOnRydWUsImluc2VydCI6ZmFsc2UsInVwZGF0ZSI6ZmFsc2UsImRlbGV0ZSI6ZmFsc2UsImF0dHJpYnV0ZV9wZXJtaXNzaW9ucyI6W119fX19LCJyb2xlIjoic3VwZXJfdXNlciJ9LCJ1c2VybmFtZSI6InVzZXJuYW1lIn0sImlhdCI6MTYwNDk3ODcxMywiZXhwIjoxNjA1MDY1MTEzLCJzdWIiOiJvcGVyYXRpb24ifQ.qB4FS7fzryCO5epQlFCQe4mQcUEhzXjfsXRFPgauXrGZwSeSr2o2a1tE1xjiI3qjK0r3f2bdi2xpFlDR1thdY-m0mOpHTICNOae4KdKzp7cyzRaOFurQnVYmkWjuV_Ww4PJgr6P3XDgXs5_B2d7ZVBR-BaAimYhVRIIShfpWk-4iN1XDk96TwloCkYx01BuN87o-VOvAnOG-K_EISA9RuEBpSkfUEuvHx8IU4VgfywdbhNMh6WXM0VP7ZzSpshgsS07MGjysGtZHNTVExEvFh14lyfjfqKjDoIJbo2msQwD2FvrTTb0iaQry1-Wwz9QJjVAUtid7tJuP8aBeNqvKyMIXRVnl5viFUr-Gs-Zl_WtyVvKlYWw0_rUn3ucmurK8tTy6iHyJ6XdUf4pYQebpEkIvi2rd__e_Z60V84MPvIYs6F_8CAy78aaYmUg5pihUEehIvGRj1RUZgdfaXElw90-m-M5hMOTI04LrzzVnBu7DcMYg4UC1W-WDrrj4zUq7y8_LczDA-yBC2-bkvWwLVtHLgV5yIEuIx2zAN74RQ4eCy1ffWDrVxYJBau4yiIyCc68dsatwHHH6bMK0uI9ib6Y9lsxCYjh-7MFcbP-4UBhgoDDXN9xoUToDLRqR9FTHqAHrGHp7BCdF5d6TQTVL5fmmg61MrLucOo-LZBXs1NY" +} +``` + +The `refresh_token` also expires at a set interval, but a longer interval. Once it expires it will no longer be accepted by HarperDB. This duration defaults to thirty days, and is configurable in [harperdb-config.yaml](https://harperdb.io/docs/reference/configuration-file/). To generate a new `operation_token` and a new `refresh_token` the `create_authentication_tokensoperation` is called. + +## Configuration + +Token timeouts are configurable in [harperdb-config.yaml](https://harperdb.io/docs/reference/configuration-file/) with the following parameters: + +* `operationsApi.authentication.operationTokenTimeout`: Defines the length of time until the operation_token expires (default 1d). + +* `operationsApi.authentication.refreshTokenTimeout`: Defines the length of time until the refresh_token expires (default 30d). + +A full list of valid values for both parameters can be found here: https://github.com/vercel/ms \ No newline at end of file diff --git a/docs/security/users-and-roles.md b/docs/security/users-and-roles.md new file mode 100644 index 00000000..68ec76c7 --- /dev/null +++ b/docs/security/users-and-roles.md @@ -0,0 +1,278 @@ +# Users & Roles + +HarperDB utilizes a Role-Based Access Control (RBAC) framework to manage access to HarperDB instances. A user is assigned a role that determines the user’s permissions to access database resources and run core operations. + +## Roles in HarperDB + +Role permissions in HarperDB are broken into two categories – permissions around database manipulation and permissions around database definition. + + + +**Database Manipulation**: A role defines CRUD (create, read, update, delete) permissions against database resources (i.e. data) in a HarperDB instance. + +1) At the table-level access, permissions must be explicitly defined when adding or altering a role – *i.e. HarperDB will assume CRUD access to be FALSE if not explicitly provided in the permissions JSON passed to the `add_role` and/or `alter_role` API operations.* + +2) At the attribute-level, permissions for attributes in all tables included in the permissions set will be assigned based on either the specific attribute-level permissions defined in the table’s permission set or, if there are no attribute-level permissions defined, permissions will be based on the table’s CRUD set. + + +**Database Definition**: Permissions related to managing schemas, tables, roles, users, and other system settings and operations are restricted to the built-in `super_user` role. + + + +**Built-In Roles** +There are two built-in roles within HarperDB. See full breakdown of operations restricted to only super_user roles [here](https://harperdb.io/docs/security/users-roles/). + +* `super_user` – this role provides full access to all operations and methods within a HarperDB instance, this can be considered the admin role. + + * This role provides full access to all Database Definition operations and the ability to run Database Manipulation operations across the entire database schema with no restrictions. +* `cluster_user` – this role is an internal system role type that is managed internally to allow clustered instances to communicate with one another. + + * This role is an internally managed role to facilitate communication between clustered instances. + + +**User-Defined Roles** +In addition to built-in roles, admins (i.e. users assigned to the super_user role) can create customized roles for other users to interact with and manipulate the data within explicitly defined tables and attributes. + +* Unless the user-defined role is given `super_user` permissions, permissions must be defined explicitly within the request body JSON. + +* Describe operations will return metadata for all schemas, tables, and attributes that a user-defined role has CRUD permissions for. + + +**Role Permissions** + +When creating a new, user-defined role in a HarperDB instance, you must provide a role name and the permissions to assign to that role. *Reminder, only super users can create and manage roles.* + +* `role` name used to easily identify the role assigned to individual users. + + *Roles can be altered/dropped based on the role name used in and returned from a successful `add_role` , `alter_role`, or `list_roles` operation.* + +* `permissions` used to explicitly defined CRUD access to existing table data. + + +Example JSON for `add_role` request + +```json +{ + "operation":"add_role", + "role":"software_developer", + "permission":{ + "super_user":false, + "schema_name":{ + "tables": { + "table_name1": { + "read":true, + "insert":true, + "update":true, + "delete":false, + "attribute_permissions":[ + { + "attribute_name":"attribute1", + "read":true, + "insert":true, + "update":true + } + ] + }, + "table_name2": { + "read":true, + "insert":true, + "update":true, + "delete":false, + "attribute_permissions":[] + } + } + } + } +} +``` + +**Setting Role Permissions** + +There are two parts to a permissions set: + +* `super_user` – boolean value indicating if role should be provided super_user access. + + *If `super_user` is set to true, there should be no additional schema-specific permissions values included since the role will have access to the entire database schema. If permissions are included in the body of the operation, they will stored within HarperDB, but ignored, as super_users have full access to the database.* + +* `permissions`: Schema tables that a role should have specific CRUD access to should be included in the final, schema-specific `permissions` JSON. + + *For user-defined roles (i.e. non-super_user roles, blank permissions will result in the user being restricted from accessing any of the database schema.* + + +**Table Permissions JSON** + +Each table that a role should be given some level of CRUD permissions to must be included in the `tables` array for its schema in the roles permissions JSON passed to the API (*see example above*). + +```json +{ + "table_name": { // the name of the table to define CRUD perms for + "read": boolean, // access to read from this table + "insert": boolean, // access to insert data to table + "update": boolean, // access to update data in table + "delete": boolean, // access to delete row data in table + "attribute_permissions": [ // permissions for specific table attributes + { + "attribute_name": "attribute_name", // attribute to assign permissions to + "read": boolean, // access to read this attribute from table + "insert": boolean, // access to insert this attribute into the table + "update": boolean // access to update this attribute in the table + } + ] +} +``` + + +**Important Notes About Table Permissions** + +1) If a schema and/or any of its tables are not included in the permissions JSON, the role will not have any CRUD access to the schema and/or tables. + +2) If a table-level CRUD permission is set to false, any attribute-level with that same CRUD permission set to true will return an error. + + +**Important Notes About Attribute Permissions** + +1) If there are attribute-specific CRUD permissions that need to be enforced on a table, those need to be explicitly described in the `attribute_permissions` array. + +2) If a non-hash attribute is given some level of CRUD access, that same access will be assigned to the table’s `hash_attribute`, even if it is not explicitly defined in the permissions JSON. + + *See table_name1’s permission set for an example of this – even though the table’s hash attribute is not specifically defined in the attribute_permissions array, because the role has CRUD access to ‘attribute1’, the role will have the same access to the table’s hash attribute.* + +3) If attribute-level permissions are set – *i.e. attribute_permissions.length > 0* – any table attribute not explicitly included will be assumed to have not CRUD access (with the exception of the `hash_attribute` described in #2). + + *See table_name1’s permission set for an example of this – in this scenario, the role will have the ability to create, insert and update ‘attribute1’ and the table’s hash attribute but no other attributes on that table.* + +4) If an `attribute_permissions` array is empty, the role’s access to a table’s attributes will be based on the table-level CRUD permissions. + + *See table_name2’s permission set for an example of this.* + +5) The `__createdtime__` and `__updatedtime__` attributes that HarperDB manages internally can have read perms set but, if set, all other attribute-level permissions will be ignored. + +6) Please note that DELETE permissions are not included as a part of an individual attribute-level permission set. That is because it is not possible to delete individual attributes from a row, rows must be deleted in full. + + * If a role needs the ability to delete rows from a table, that permission should be set on the table-level. + + * The practical approach to deleting an individual attribute of a row would be to set that attribute to null via an update statement. + +## Role-Based Operation Restrictions + +The table below includes all API operations available in HarperDB and indicates whether or not the operation is restricted to super_user roles. + +*Keep in mind that non-super_user roles will also be restricted within the operations they do have access to by the schema-level CRUD permissions set for the roles.* + +| Schemas and Tables | Restricted to Super_Users | +|--------------------|:---------------------------:| +| describe_all | | +| describe_schema | | +| describe_table | | +| create_schema | X | +| drop_schema | X | +| create_table | X | +| drop_table | X | +| create_attribute | | +| drop_attribute | X | + + +| NoSQL Operations | Restricted to Super_Users | +|----------------------|:---------------------------:| +| insert | | +| update | | +| upsert | | +| delete | | +| search_by_hash | | +| search_by_value | | +| search_by_conditions | | + +| SQL Operations | Restricted to Super_Users | +|-----------------|:---------------------------:| +| select | | +| insert | | +| update | | +| delete | | + +| Bulk Operations | Restricted to Super_Users | +|------------------|:---------------------------:| +| csv_data_load | | +| csv_file_load | | +| csv_url_load | | +| import_from_s3 | | + +| Users and Roles | Restricted to Super_Users | +|-----------------|:---------------------------:| +| list_roles | X | +| add_role | X | +| alter_role | X | +| drop_role | X | +| list_users | X | +| user_info | | +| add_user | X | +| alter_user | X | +| drop_user | X | + +| Clustering | Restricted to Super_Users | +|-----------------------|:---------------------------:| +| cluster_set_routes | X | +| cluster_get_routes | X | +| cluster_delete_routes | X | +| add_node | X | +| update_node | X | +| cluster_status | X | +| remove_node | X | +| configure_cluster | X | + + +| Custom Functions | Restricted to Super_Users | +|----------------------------------|:---------------------------:| +| custom_functions_status | X | +| get_custom_functions | X | +| get_custom_function | X | +| set_custom_function | X | +| drop_custom_function | X | +| add_custom_function_project | X | +| drop_custom_function_project | X | +| package_custom_function_project | X | +| deploy_custom_function_project | X | + +| Registration | Restricted to Super_Users | +|-------------------|:---------------------------:| +| registration_info | | +| get_fingerprint | X | +| set_license | X | + +| Jobs | Restricted to Super_Users | +|----------------------------|:---------------------------:| +| get_job | | +| search_jobs_by_start_date | X | + +| Logs | Restricted to Super_Users | +|--------------------------------|:---------------------------:| +| read_log | X | +| read_transaction_log | X | +| delete_transaction_logs_before | X | +| read_audit_log | X | +| delete_audit_logs_before | X | + +| Utilities | Restricted to Super_Users | +|-----------------------|:---------------------------:| +| delete_records_before | X | +| export_local | | +| export_to_s3 | | +| system_information | X | +| restart | X | +| restart_service | X | +| get_configuration | X | +| configure_cluster | X | + +| Token Authentication | Restricted to Super_Users | +|------------------------------|:---------------------------:| +| create_authentication_tokens | | +| refresh_operation_token | | + +## Error: Must execute as User + +**You may have gotten an error like,** `Error: Must execute as <>`. + +This means that you installed HarperDB as <>. Because HarperDB stores files natively on the operating system, we only allow the HarperDB executable to be run by a single user. This prevents permissions issues on files. + + + +For example if you installed as user_a, but later wanted to run as user_b. User_b may not have access to the hdb files HarperDB needs. This also keeps HarperDB more secure as it allows you to lock files down to a specific user and prevents other users from accessing your files. \ No newline at end of file diff --git a/images/ave-age-per-owner-ex.png b/images/ave-age-per-owner-ex.png new file mode 100644 index 0000000000000000000000000000000000000000..8e39d22c360f412c803f819545e1f3955fc32d11 GIT binary patch literal 38173 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 literal 0 HcmV?d00001 diff --git a/images/xcode.png b/images/xcode.png new file mode 100644 index 0000000000000000000000000000000000000000..eaaa35263f17de94917add46ca1294a216edecc3 GIT binary patch literal 50262 zcmbTcXH=6<*DoAEKtu%T9g!;4fYJ$|RH=%9N|k_gB1LK_0*Z8`_aIg2AXQ564uiT{gJH~r)P5}U+03CI;mwr||i@x=iC#gby*Fa^LhEj(QA5zO};&l200@UPH z)+6d7_#=z}5rC-gp}nM0_tYsqst?Pv-j>O5jAa;R1*2VE(?y%hE#H0^(K9rxR6g_S zPNR)1y%9WP0t(sru&=zq;;AszeuF)Z}zr!h!gPGFcqqROor|}{k zpvP)=#>2P&Nc0i1oXdWc{A-ZX&z7morwzh)@x(VaHfBiKKfBE#BbJqw(_UsDYj%qv#%b{e^xltZy1e1_+{`3Dc`MBLWjCVAWc(Tn> zs2L{`?7cZ&Ou_zd;)(N;Tvae4mTe3ka-B<=Q$ZTH`24i|{|NI}q8Bue{v(?~A-m(` zznSK0J|6x4?{x}sOXQDEQ_DIF|2K-igb#lFUoOU_&;NgJ^Z$|i1N)Tff419tRQ6Hx zf1`aoHR<${Ebc!loB00!X4Fo~zJ8}w(d^&4n0i^VNE+vi{tt0U(6_&9lml=xI#VS7 z0h;Bb@!!DxH>rZYx3??*^4_H9xx{LnMW4peRgATU zYBs3-TWK#R0*n9O=KF65|0m4;rw0BsXkV6B{cklaiT>sNA9Mc8&h6yvl|i?hUHpaG z%Ps#6rat*~w`6PZ9mYb} znxUH4SC@sp`%B~mE9L9`sBwpL+@|p;ES`4yr7@EK&ga`~FS4C5OlDy{p{7dC4q$<@z`u8tKt^*6L+5>t*Nyu&;wFh*RWL>4) z?4_f$g}1b{C1ltrYhB~^t z1yb&w(hu-tTup{*lE&WP{Fe-+>Rpkv2~Y3H7Xeroe|?B4DJfxhoy1X=ss!Zo8WB%- z&#Tb;xjuk6sb1EHHb%EO<>Wr!=DxejC)oY{G>SaV)yQk0lj215yYIH*rf(-0ITLc5 zVy2?{KmT!JwX;z6%~b zceH3^t}Z8dT7#|us_5KSlA+4|BIiI+6LN-x_0uVzKtrbdG06MdSQUN>T6XTIk@d)$ z6>wqDWD2Wnux2bwNs(DxUIN42At$Jx&9Tc+qHYZ59c$fR#!kuH%Q~kK187xa(!z-K z^?OCWBdz-{8>8(0E6e>?_R9%6o%Q(h+My@wv5W1=)>N&}?%b;;cgq~V)hLayOdwd3 zGYB_!IIb(%<5JzuJdiFa866IcovyZq-)GZ#f=IF0rQpvmgrGJ-F}_KluFo_vCgr9~ zlS@n741#*Xe)1OwUB1k`Ze9j!f!xWrr3r*b2_rj^LTjy!e!qRs_7#A zV;lUBG59SrI6JM%#rs$`=@P7q6;y~8#E}2;#7fYMU8OoGoy1^HXrK0*ae!WSvDIP8 zP_qYm)eClR0bLihjyfJzgp|fNuqpYyKg`|>5(4jOr(WLs@H`Dw3hb42p8OtWh#U_d zNE44SHdKDhEMdVK$sOSMJQPnIJW$6j`{6~}&)aL`CAb%jA5KpcFU62L%eo~g;{LLq zQTQ|EAl{Mf^z`mE05~m3`23z18`5vCxO$Csc%!8*VFTjD4~Z|W2p-8(R_ws{NFr!M z@LkQzVbnfbnEdcNB6DlavmnBTBIa=icyoIA!|q%|J`T2%9-wda;@aD)(c~!4=Mf7R zm)9WhkT4Pg>NsEAM?^D;M$2#B^SvHtMs#WIg(__LEuqvJV^b%i2vSQ@Lg^3OoE$#E z2WVZZlu>P%ESJ@7>+668=OzX+2adV?mOmz6&VUzn)2EI0nP`$Umj(Z z@_WhmIzciNfu)kR{U=2p=tNxc!0u=OgZ91xKP;`v;cEE6Yeku33f_7oa=)5nNb8Fs zYE)AFt(m7-Nll0 z6hh)FttrfjuPC0zXY0(ixNf(ilZ=?zqJ*iNjyon|y5DiSRkiN@dcqCLjK71Mn! zcw=G7eskhzYE$fLs@vr~T**M!h~;^Pl_;G*kytvDf%q?kO{ zY@+b|{f(mJ2?NE-?+lO*!n?Q0W&iRy0DJqqj}em+zsir9Mv^!aHF(F^&tejVtn6Ez zhkJ)->iaj?gU)3WaiN;|wA|d`k?|6th%aM`p@<9}FWZF_0`M)SAVcOFo6+v4A*OV? zmhoY&78pFd0(8BYbacUOPXI&~&3!}Tw$@Y5blDhxkhR`S>TN$;GG`qBi?m$$zM;~g zy2nn(`JQO#Cli`clF;{!Q4B)Nidvm*VipQ+l+Cwh8p{2&sx zn}i>wc_OC0ASR=7jM(@1a|yaw^3a($U@)@|)o9zTJan*-b+51J{itM^GnvscA4B^g zp_;(>s?KfkR|CKncDdsK-pgtE#N#CEf3&tL``%8;&A7yp(Bc1r;&`;MqxbCS*#axR zM<}y5O@rUeKg_D2{cn``o=%Na(RJW5A344Frl|8dXy&(XfQ^bqf>``!^=^NvDB288 zoHC&6`b=EW`c2q|Uu}~{?x?*`g%;uep%2=c_`5b+zZcF?J*ti2fSB zL_b)RH^aiSde+OAAU5|7^r*ytlgj!cmJ9nd`$omZ3yxqlNuS`sl~+@!6C}$Km*E%j zGgQd+lP1jMlE=lU+nnsl*Ou(~19a12U$!d4wOu!?vDdw6(b69o|8B~PhcT%0?@=#n zQowbuaCi5d+aemz)uJ@k>gI=Y=DPcIp2Gliox!T?;eMMH^zHd1rTPfw?8PLC>d~8X z8WEDXuRy=<3`ZWt$C~Xlj|7KbaT>ql;))zr6cij*R49>n&Gs|?4b!n{4tpe*mu)Pp zAs@gw6nCXhM!{6KqiEPF7@y-Pn|xQx?q`g|ukXSK5()2x4TRoe$sBLu{;TfRSkV{( zox4J}jA79<95Rfs#j2_eC zShWnbRt+A?W<{{trdCXw6qOv8YiFE1XsbS+Fbb zEL+U&1eGg4UWVC=v{5OlI;O?3uSF}Y?XNf6@vAZ9Be)ceCk5%t268JA1##jYG6Bn1 zl&crVBa%DccBMy@7(%YkLU`a;C)F{zf~|VEBBP`5{h<9ojaqNchadc-;wP%uuIWWk zEmy>C>U_;o?_!=nJHqiXdw@~ny_lI%16C? zpY#vb>wb9rskXN6jiYT_3OZXPuWia$o5#NsI;|N%o^i1lGe3Za4ijqpXTq$+O?SQ= zy*|oBLUC{1f31hae_)h!$nnrz30Fm+E*DWu=<#rTKGOQ?DE(8M`>)NCe>B(ZS*~<6 z!8PQa3(wo$

@Wk$uX{YjT@6#V%f862Vx0@DL{gC@7CLzd~~$$Xz{eE3|G`wcfg# z4ZZD2lLrgAB0Mqlr+QJbxsemx>x({kz1E5^)@o!yUHBZX_Gmy?(%~z)$`-4W6yKZI z)68#i0BZ($Y|1E6}`J*z&M$dt~nQN z5^`3Oh8>@T1L)h?y-uCCSu6u-V-vU+B(o^E*eP*N2_-9WOozBa+s!Q(bhp=j!qkQe z`-T9zm4OjYP3BA=JfwzuXq<2|;5Rk8MCgpjPM zyfKFP9=Lx2y*f%RgoqU=2hY__*ls=75kl^yy4WrqmUbWf_*tk0|LtKjveeT|9?j)w zu1|k$c|QcsUvneQX06S%hW?&VzwM^9nGNUQI)hE8uOer3t{ni--1N_01bB{ZL?#=+ z0{Aq%M8S?oV-?^-YY*?;{R)-uuoJ5HjS{qcag2ji-_q=dUWsvRPOW3=v7eqBoip=#r^QDKFS;?2q;QDsW;5L*y8o; zZ=2zUI-jA4qec9jwo({-!f-b2K7F_yzf)qD9R25JGCBcesT!80AlWKXfFs~OeM;?# zK%t>16VVqQ3H~8!6CNz!5_ITfrSrzqb2H*{!px{@P2jOcgg#C>&r%wylh4r@m^88Slu`>!5UcF$4)V#^MI7uRNdY#fj^%n<=8VHe>_kF$^KDdXlO@ z>kYqUZ(}!i% z=VUjAp86a|^VO>D+X3z!YQk=Ycx(9t7i0)e+BsWe;H^KbG1S{lJOtiRbVidNL_aq$XzxzKfMn}txEZp;?_c6}6cs3L zXLB%=!q|VZK%$K$nq?*0FMH%R=7iArWA^XR^>$$J-c;>Q5hXfF6vV@)RT(XzjMOsr zIzOjm52D<=!QS;bA$p9Vgh>^=ladMamvWaZsPpXy>M9*(f2%(;y+p z8UClc2y7TLD^!)hAoGasd>mb1?-=Nj>{vb^)#J0k!|eFpfJ*=9WuhF=ftzwQe^t_)9|w%VKb{FA=FA+bdCCEPUwCy(eIByvl0Gk zBQW0AsnD|xSE5h3@^;dbohOx#uDZnFc+s~R&U-x|nHAJ%dor}bT z_yI-szx7cMAkj`Vf6l`zD?5luz(+Rke&LMp=j$WM>leLby$AN7-`3a|?wRkm9x*Ni zk+6IBJ8*4utukWs`GPB}NlPK5b^*W++l?b`YblDL%1|dEm5#qoiLgfA<5R6 zw>AuIs~m8D+Pm+Egs|EAI9bY$ZfX=JyCuJmYkJ|Zi{~K{LLh^!nL!a2e*MtCkXYS) zD!JH!bSB8fE7!gc7rkwr@cmp@!HZhLK4Q%=Ck63R{v!V73Q9ldv_I2Vbx zzktFud-9+ET#Yc6h@qAY7OO&CH8i`=yLjyNSbzDzVI55_w7~qdG>0dWyG0!FNLBN^ zUUW6Xm^+Fl8|R9)Fq967x|8Ng z1v-&Aa-sn2rEN5i{vg*V^yRh&SwnEd438rMaJ&21mIy2d4}F?OjHi}r?UQt&m-m8f zmzPnSCubVxw;svK8?EfQ8i+-aaYTc(SZp}IdWksF-z)X8pkr1D3_N+~N>X)j%N=<{ ztlKw5`Bxf|_JwNDmk{_RKBo3stk#`~%_ga-(LEx4#zCcZ)MQ)tkJ<8#pLVv7A-6w8 zOYpyhV)^_@$>}5c`FM-;qJOH3&+2d^NJ%+`0y78S1_uDmNM-MlstjkEk-nwY(Y1n0|{3lZBGp%Z5!um5yPVHZz6nS_i z^HRy<$H$_-RI|g7AvoER=I45NwoDTFUjt7z0@)meABE-}&PFP&8mXDQAFLE!3Eznp zr9&dTGtN|ml`j5dKwt5Oz*WOzqqAT2@PRl>wSF}uhYAG|1W{5BbLK0nrGABqwcE*t z>q9ku1%(S=`vUbs%^Rh7xN0k#--#Zh*=~~E9IWQbom=OxmboC66GO2&yDldTVO0m` z?R7%vgaF*_9MZedX2fq~Xptq`B-n~M=ux8#4yo1s@zvNJm752Gg&qfhh(j8$iFA3U zVC2y|S1-WL9X;exp2v|VTi`g4@`R2=9`_Tmb(|yH%h>a(7T>2_Bynk1;Xb5sBwfhvgW6>9 zcq{?vWd&w>6zIXJus=iUIB3x(Qz9w3fT^$O9&YKn};9nQw$#&ZL+k$>rPuMbvQzeblqW)Z}W?HDUvSQKLt8o_b4f zC)(TU$_P3LE2q&>eg9yIRD1ai`KC+oz5X2Ht*V8-+WU!iSdHJmVWCZ7iS@@xMwy=3 zIaNoVA0x8O_ip9$3qD}e&ELSUJ*96_e=2byDjXVJ^mN4ZEsn$$D+I(>VYMG6ar*P$ z3ov}Ib%skPkGsT?V-G55M94ZXA}KAmBab9}1JWiB??>r`dS{hu!a|uIs>?zJF6jX; zZu7em9b7hMd9YFmPrw5iVm<<~Zc2R_RwN+g3<+)Y?W+@Xwd%L9B-#G))U^SYP}bvj z9?2P-OBOp~+;|nbuZnjNMw4FKZ$fIH?7I#&Vy1`7Uck4ClVf#$bxE49!ayfz4onft zDakS(P{5N;if880SzNzVGF`YJlZ>WYY_rb)c5{UZ;;E7+rFpWVGPlZrA2U@vUKfa+ z41XnpYAS5r80yT-?$x2h^=gIM2leI3hRi>gp><&C8V zTyE|-&IDm)`R6DakSQ%%2*i*tB1@V(mO$Ou6VYc5-s$E5JD&exmD&2ts)U(ios*OC z-Z}vWyaO{yuhhM2IQ=2FTcK5es-ckLPyz+uX3yYBrXMvY=0~~?qAtR_Rdk57986EO z4C6f}SQur)xP2zGUcFyv0d8a*+hS^25V%xxThDL=SF60B1YE8pw?-aQ`HTOK%E~`- z>pL#s+5Thj>ZAsZ&10osCkuzuEfyMk;=5}t1YcRZceN~|rtVRuOml6<7QB7gY65g0 zU30vh)z*`bUHA?v?f8X}i4l1>n{Pex(V}{EL)%JJE9^zx%bKI4u+Sts0g{i-#rGfH zaIAuoFhX@WO;btDq^1qtQyKKVI!bS5yc5M|)@^q$h9Gc%S>02Ol*^D{c(d*1niSP5 z&#~Mh@gLN#tu%Jpmv6p_Mg*H{Xltv6d1&z!8qII{mruK;ebZ1-dewjf0$OfRIlntXG|@VV`nHQ{%4u3 z`RQ9xupOzr3s_jhd#`C#8}4Uk_KBexEfihcf(>07?7{dYYJZfLF}di|f^;&$zJ3uf z?L-bUsEmFN!qVrjQ{G}UA1(ezXtR#xviW5pJUH~zKTcH^m&Y{3ceFhd*&J~y;>q|TsOs|+c6qLb=U5kI4ISk4le&zb5V#({!F zX6o0=GvoHGP{7NbYFhI#@$)gmCG>Bh?ow94NWx}Bu&0ptwf_C8A&1*ia>Gq{4Ma01 zQj5eq7@>)1sIfIWEaJwrvr;`eNIA>+`Y9Hp9g+{Z8Zq@T-yK{@SUQ4*;5!+vSDV(r zq5=K6QjIGN{y&wiE@eW(96ZM#&w8r$gzSDVqMd2E+v++9Vk^HW&LaEU6z>eBAboUa zYwo3m!seK--)&Biq-kz|LrxtgDnt3~6f5iKMI!Y%0S?sEhYM&9H9tUb`DoqF_6)ks z?(6S5v!Lo%z8{^Y<`ttWdA0iHQg~mWC2I3i=j?8t;E3`F!o!%Q_5}^8qaN8_e+IJ*-^Ejr(%^CcjIJbpLUMYyb}E zp!6~P{m(1^sd!c<+SJagVGzXJUpg>-B@V%LYI!W+c2QjBz;e>7*dwG9SD-6u= zDOtRCn^nb&4~E7}n8Dk$!nu#>^_EyGc-k=r23EoOu(r2f-I<4>y>0)5iQhV!14n<=xaB;_=YGA*>jvtrSrNV$p?d}xsT9yXG}&vvi^E_ z$4FG@Rf=4-1B96emyt!B$`s|Ep5)>>({;#%a2z*=Zg;S?@Lm@^Vq*W48hK7H!-1Mu?&dMB&`J zhBNHSZ)nB$!Vi?jbZ zSf568GfnqQnoJ{c$yEt0LT+(Bmm@#Nk1Z|m zzd10U8`55N9H#b+ib9LD4eN+RO$uoBX^f3oJ$!FKQmCp%07R>%d}pK`-f7Ec&Y?=Y zacnT})9FILbmM7(ck2yIueyWzWZA3tH(#pdiITbT1J(ElBKP8xpJ!V*x$xEMzKm}1 z@Fcfc|2qAu55lSRz{tOS(!ok9>E+>538`_*On#=KnGCHLX`}p4yf8xqI*W)_P1FWosQu3*L!0+q4Z+G4SYO`D^K5kU0@o!nEq6dd0nr~ zQ-=6m*^qwsUZcPpf|}LCQ^qWq*Tt=3?iO5 zOl5}{Qe@2ByQb>WB2h=qyi@_}lwDY=phtl!eT&pk5p^8zk7Q~?+{iS)UCy){ z{Q%b|oPWRp)eRA7C`jdyOLCYkX7MPAbZIT5t@wJ~;O6_ZR$)~#l@dPr`yWUwM2IcM zE`x3A4o&gOy)Jq^C<~&TCfxS3__0;(jc2Zag#)3h3YFZN2csfs-JhBtr4tA$qp9f= z^|IWV01CpBs|l1vrtGFEhr9qxz?M6JT)nru>8L|ewF{`)r9{Qrn|9W^91fY#UxM+` zuCh3GvT%_a+CEvR<9U7T39CKq$vX-?T{Y?$;EHQ|`2e%_Bww8ed0+@35{AvNjsNB8 zK{l#j${EL)rJ%V2-GoL>ULNksaEG2Sfpi2y92UbD-yw?7N3nxTWkV5@9JZ6U>y?S| zEh3sDZSshbC2o#JC@+SmLEz4ovP{4!4}JTaY!vw;dP|4oM{Q2krW`eP<;&(6A0gURk5iSy&$&2J7m z#&xT`q?UBIOnYJ{M7il2gOegj-A#t~;z>wJ0!^{ZXQ`U8Bcp>S>lXsk+NBW}HR15xf+BpcE%{>^sQfKOjb4}f2_oc3{ z*I~cI^U1~dm3ycpG4~4PkL!#x0kRvLqYIeM1e1r#I#z2dF409rPb1|it@s(hE=Vi? zS$?#^ed(&T@_dnhTy2%mNzZEw;1`g#u-}@u9itL+?1i5kpli`pNC{R2Ylx<}DwZZo_( z5-CD6zwv7Tx7hy_}&i^!$h)Wy$ZVmI0l}8a%#I5#CbIuMH>43he^8#4F=$ z=cVpvQF5{yV=5AktNrBTU!S}pk(v{-;z5@zau*7s?(zvP-c80RF*3YEae*CB^weo8 zbQ_jLPd%8g0q2a%OAj4BnBW@<4t@PwwUiz-U-IJi7Xu$_j36%bgU&T{uSt61V1#&j z!{eRt$);vWbfVReBI+%vom^A+8((On{{sGt?Oq2TOZOz-aBQK$JiCx3UR%tVxv@v5W|Hrnw8x$@dud?0P>^$#aJA4<}^P7gtdwX6vi}LhKRXgbJkel+7 zhtrQybAdxe(>vee=TaW1nPw^|ShuSuisHrSr##NoXN8bS_~Z*8P{M{qhpx6in0Xu) z+w#U6q;pRm^;K-W*7$S0_7h=a6GN`eRy}%`q$|9XLtc7K5$Ua%r?tjFKK+~VDVQ(w@0 z`iJL#`l@g*{wOZ%(&8ny#pXw~m_k#**$cBxd?CHH{g(q*pQI>L++K1cId#0>f_9JF z_TyKCena+R<(fp>(>81Cf{zk`v&Y{{f^R7e8i9znK0+rZ*>=&4))yTyIvK; zCA2pjZ)1-Y^T*~O+Bj#YgsPyDZiHxFRPr!UPcPS3x8ATr0-@g`PZ8TDqp9Cket5pb z3Of8l(c3JJ7;Xcl!HA9)&_xz7E+shz-YD}p^mAlT>Tk`%s;?M|UPlg7BjbT(M<+68 zcG2UHe1#)_d!zy!BL~<&N^-w&Y>fJsTFkJ@r%0x=IJxOUd+oyn=2i~HP)(QG*0FMu zB&Fj*Efm!lWNjfs?tD$oqbs*|b)gl!l*K_^0}LGEqDdSB=#-+ECjKljX;rl=nnGOf zO}Wt`zi<95K<0xyw2!aG6!Oh**opPCJCCjsBj9>6I`Q&CU&>TN#ilGH^p>V-pKpEV zyysuVzQFZ7zP9{o{doT|oi7q%lBaywt{B{pEZgwu8Rb$cq0ku-*D^YO-DPTKOl>&kVDc({&h-O;7Cr{m_} z!Pv?%&<7X2oIvy2u5gjL(#1L9)w7<3Lhgju1AB2EUR7ThMy~8!e-R0qA62A&$uwvg zqwe+?JRRu>Vz(h{LD{~?IdQSI2PDMr>4OV>kBhKN4lfShSBD40N09}@#$VoHqo;g4 zEK8HcOg2aB0*K8VEB3d*am&g7s*jD@3qqLhE zXL4LTi=y>GqdCqga3e_2gc6!=CAx8)A)M=PrV92|Gfw@i3MV{)$|2ht%lIuFVVc#Z_>My|~>vNQcHk&s)7l3nZN+)czD8b8tzFL9OsJ-uE z4*ofdU*+lVjfL##+2x5OnB~$IFZz%_Qdm`SvnVd(6l23AF3r>ZLf9F`o2&GFWjncahtbaygDPd(pscmFXY zE|o=@?$`{)`Ez=KNqTyL<>wy|>#;$IFf+_QL`;s5HA7q;078lXkh*ftEEN9snrO8W zG^2_4-E0U45W)GL|42eqOy_r>#XFM@>~k^E!`Otm8aBU_MOE}`om_NK*k(4x`#no+ zkes{=UneLeRqM4nw@MZ@r!s8uM3>ch7a1art@qQ*0;;#x+sE^EG9W?*1f}eMM4%8i zBZ+tRiFOj`)b+A{b4+R3$QGeaY2Cm4Vkf!`ZL%@Xuv>PWCw!9KCdj&d^CERaWF*t= z?DMop=l6IRIPK)sdU5|qmGJz#>m#|TMy?@L=A@G@GHaj<1kcZ7hc7bhs zI7FmmaGo8Eag}tx;8Zz7WO(B%f$vUfYIo{OSNj!1n6@PdwmCsPF}13VaKfuX}heZQ)gXt9+ECl*j1rsj@v9W#~+1z-8g^Oih^swR8!@X;q;p#UBRCLRL#NTMm;^f(v z`k_`>=m0;(D%jIYVTcEwQv~>BV!CQED^=?+3fMJ$|TA@B{`?jN&h1 zZC=p@n9Tdg6o;cVYWAxDDfSh_zN$6D*U9)Yc}^Dfbs0hyAs+D{p{bOxL~DX9;6NR^ ze#)ATQ=FsN7k<6x4wZ6{dT>@<)!{o80{NjtD?ihD>ecbOdB@l3x^1i`Yv%M*k~K>D zb=n42+nSBm{b~ZPfaw_De;Q(6Ac`iXvwOzoS^6abZ}lhl_(EX)$5G0fMa#ofxX^wJ z;QLmIg6WYt7mOCKcZF?HGW(FD{M{0Xju}RHyE(bF^PLDnbsFRtJ$;r&@CD|P-*N^a7=SkY^1u(X zf7|yv^XOlJARevUMu99!!-Tvbq|B|W8oBRI?{`pv>KC)W!_gzOP6>aW>w+;!F{^7o zd1754f#QuzC+*EEOFZ4X377ZX1257&y2wgT98cjS$#qQEx z-*bl+d0fRqx^s%tk=C3_;-~ux0(bW01p^)>d@1SJ?a8)_Y3gtE6M(uL&Q3zKJs`;Z zLoaaO1M}4|j=Th834-3BBB4X>DAo`e_5l{%^#??+RTWqd;NWNk8MjFm`<~Johk}uOn_MJ&8`=4InG&cHs(0|ha4qnY~#S}WpVHst8ZTpai4eD%;;RXNbH%#^E z@y#0`H50d*YV3oY_z^Qh;s{Nja(;?h&vS@uBlc62*Z05cXC5Szr@9+Q5*|KAeq1_g zo~rGa?|~43pTW!&n@=A&y!JJbKk|LuWd9E}>{K}@f3CF9|1>Q&(<2M|q1!O;I{*7b zsOD1sY+=jCNy^8!QFu_mT-&Rr%`~}_FfA}Ih(!6yHHRMXuwQY0TV*(865ugD`IbR_h*#q=A$K9Mc{YJ6aIk28Lgxxaqj%>`S zJNL`bJJ&Xwe@d-A9ynkldOV02t<~{)IMcu|^!^?5WA#DnYwtnDOX(h88#HbJ7B?Z$ zBJ@UZ=!;=}BXcXCeY;8cw`XDfsM-tiXP)$ogfp3tjq=8Q*QCkg2ayEaz%VI0+r44? zyJt;LLrPO|6&5~B+O7ZgiZW7~6e?FUuiYrv7NJd18Rpr?OzPNC~6I|Re)@XCWqYpLKEULxOy3kz1*sPcJ=(e zRZorW(7P+r%mfvobi>1x>Plo}N9%ER(c)olM`XX7?^H)&%S?{=#nhy=;NsIHav*Y% zrEE(`P2?F&#JE8vbU-WTSdY0y^|sud8wABl_}x}y@f^##SENy}P{M{Kb_3Ffzh`ab zIm?k>E85?N4g3&$aTy%?8N{4>f%Hnmmh^Y=geh=2dOL019ePTsIil8O`)52E9jnU| z%V*f}bEbz?YB$Xh=4BR}?dwom=rQ>N7CQhTz7w#^ya!rRcpPvC#%4(oANAIl zg)v=tp?p(y`g=s234=v=VhDVLxV_kGwxwClg>hgtP(?vy65&EO_)&73STJGQbp668(jBLT% zn_sv+(N!0;?2g`(9SWvZulOI5o3}j(ox0yv?&0%6>47gNBjEbQK@Bf0)zR&6XH-0K zKWb*wOBp-+^A5kO*Mr2Xu91xk2n?Fk>Y#^*!-a-Pm8R62t0O2SZ>*i=0ar=XueSP5 zxhzGxVeklI>1W$lToCKce>qj%7O$ae5uG0ix*e1{2UL7R6I@}RxJgO?xBB5vx3^okTy03wy%Lp4R59PmZksHX-3Sd#`@8`L z0N1^oV$R4O^KmP zOXj%1jK-+Yb(JhKPOzYuSe6ckQ4ogSQ3#a`M@K9rQ_gJ>| z7mW=S7V428-n8879=W4I$?iHYIJ1!DY5R=dnIw3Zrn;w(X0#SVgdl!QLqUJsLLTf9 z`XyJZ(nH?txI*!T6X=lhXu=GkO- zlJ{j}(~CQi3~!g6g5>NoboWx&^9|0L@FN`)f(r-g>};3^sQnvm9x^v=VQ27 zGOt;W-cs#1CYeIJIq7lG4L;MHmT2z<-;I8v_poozP*;IcT2`f(Z;!*$^m3~ut)cp$ zmYNv@@EP}ksLbF>;Fd69Fs$3ayM#`D4~ht71(twYyi$*R>d8nf{9Y7gtkB{xEBbcF zxEI71-O6nAHfZ!_vfo3=U??$HYE9{fAyGfjSxX}HDHB4ueal8SnU7t9_k$E}X0lLpE6*e4`yZZL*?HQ>y&a8zx^!$;?8mgl^U0?+L9RD+pTKxD3GK|_P+JB>+o_aX&F*C$ZoH^&PQtoI$ zb~Qp=C1IlRQZBv)S6=Hkkq#zcB#|EJAwW||paA#zl?rl5@|yoMSBE@k+|w26cm4Ws zkK9a8kUzkr=*q6*{FMx$)%9DU47W0%b*19o!-78u zgZ|^M1r!X`>h-_Fx)GW3?-jBPhX-dHHl&F+?YsgaYPBSFK@za#sN**+=z%Q-*!|u< z0g<4};GhwkuXZ?K2k=H*=z%%?=?!JDUBT3J;YVd3_TCpa5Bgz!Pu_hRv%vo`%y-&Q zvf`30{YR(CTQeAKmxvyS?qyKwdmDRshJug6PH~V=VX_;?_GWg8oSUQmqNYAS3r>pE z8XpMpAL&%tJudafhPq45uFP}d5*$SRE;kiXO-cLK+Vh#&TnCVy8zx-Or%aj&$|=6RC_H75EAze)npr8)7M+HF>Vh zh!J};H^ve2_qFcU*f_ob?YwHo7k~~#43Vz30&yb)An$>YBQ92|lDdo-A+PP^Uwoy9 zeaWiDdM5d5K35LzUIkL4Myi|q^UDvYBmIZKAn2+g)c&w1;YpgC@`-T{#yqaU5cQ3c zXy(`FVu1V@f5XFxC1B7$?l*Ea(%__v&x*j&F`E%T!7}(10_`oWyn^oQGY209J}JA} z`ADnOOTJ1?$dmcap*ncB5wXJeQO=o}=JAHFz2ojuFF{b(*49(IbI=0@iEyv_+;;R6Qton|yI zr?CRJF&Q2^#^f^@b@-2R+eeV@OcoUeJ_IL6dthvIW7@YKd#aHdxuR;WI5iJd*y&95 z-@3RwyrjqagS^t;1H=Dsq3i^-JEHNx5l4{qXzmz$mNxoyCE~o?-*c*`PPBu`OA_xn z2-+_PI&?Ly9#AjEe&PtG^BRHj-BCG1dhFQ4qfN}#Ln}@q@B@3gXvts0BAuTpgZ*Rub#>v`d57gY`D0S|e?2n% z5)4j=NtOk1Z9qb#nR5>ZK zcvYnszgfaL(;DgUxh?>pmcIY+u99g^PUcfgAVbc8uY#Yq%*IguAYjFt)TlFSWy0``k_U2zbxaMC{;O z&gEM+Z>yn`&D}2-$BbsM$u#^Bu&y`Z&$RP`>7_o0((-hm)e#pjG;YN2n#^BGB6G0u zQoylQW=F(BF@0aU&$oV*@_g*BQ8$A7_n^v5;^k&%pi_k=*NCs;0YjgeV&~_@URR4> zLVVnSR_9fIS(J@2sB3sf`x@7e3cUkd1`gw2e8G-D_62ngVt4{PMm_;!_s6DJha9vL zpjD(as<&zYgWXS<91qlL)k6?NO^l+i2!b!$RxHJmBx4Nr00;gX^~ZZ`zs5iWW(V%a zlH9YsLRUGm>0z0zbhnsW2Hlj-oR()H9YPyl#5XR|L;*HFGNu?>w9w6buTC`!q26WV z)sGU4R{k0l>w(dU`|-8pnP(J^iBBzRc&|GnE$g67Uoz0?ggW^E%26q;alX(L0v*I| zS{60XE!o?3?e#U0pVFl(A!v2T&+`UM+N|TOC&QmAiKj+LzK211y&Qemn=W0Q{_%6p zz4)Q8{)#A3d)F&1vzEdaap`5(9cEkJ`D2yOLn-S=qNX;MxP@|p{}*X*{nzyS#Se>! zScr&(bfL(B;X<__CNZl6%i)wFgx=w^8r$`jL*ipDYFXEXJ?J{F8 z@B-Joc@_Sh4#HcMalVlPUtJKj>S7eV!LoKQLfyLCPa!N~!`L@WK9;26=J-8nha0yq zJ3m>w|2#ynMv36uGXhSLe|OE0k(|O{ej^u%L{;D_qWgYwOYvG3{j2k`S%ixXsqJo!je>OfT_g&E>GuQ|H=?x^4E%}Ovk5y`&1P{4hc_BbeB zn;go9=IbJIF}?jaBn(WjiF>z6%D2~}CZrP!`uQ_;zg3keEyq`~d0J*`l~w6#oH5BZ zO$`cB-P3XdtpB>2d>bU)xW*ZLdJtul^ga+?ul`bV`@J`u{sAC9oF@PFw);S2taXta zd{lE~YvFZW_{T!-(O=rbFbhQQ0sz|uct&;)=0(CbYf2LxuSmP3rTpNo1wG(Rl(OIo zx7@3HTAmL1ztuDRxB(tI9C7yOp4mJ1=lQ?QKTptV@pgP6XG@Z&889T}xT$l$O%psk z0lmV6+*T9*K7e`p@`*!te4BO;kz|uB5?Uuv*5fjToCU5KhNCN!A5hFX4ZkGw6#=`4b6H?rr`^D6glLK>tDixv@`c`*{N~JT}FFN zyZhp|ylN*To}(!;yaat_C6HPgwznwmts>SUqW#r5d|y2MwbzATOMs9X{tkp&51zE2 zZRoQPxm>odr#-yoG0NW3?-o*64pM#ChL(`Kg*ofDyP~`yB|Gbu+7Lsj&?>o8Zmd-cGi(RqsIzy3QAV%-)!86xplj1ip3KVB_zTz&AdG# zEwVhqZB2T=S;HHWLGr^bXG!$F+t=Gms{h3}--0$(Z#DS&bcah9*t`X`+xYCywDaLT zFKte?hIqR}xA<)g`8*x`oO(Dw%od0~mEMUV@W!-VAtdHOva}ZG<g zFfOFn=n5zv8eEsQZtyg*plfU~sCrvX@oC7X+jlDNj9;-?tLw54bJ(%m=V_W810*2UiYPw>7ygYNBnN0AYCxPOzCyv^6<;SKKTe}U{1pvN}ej_bn{ z=g-uv=!(YgbKfTuGXAki8ywHo!u7!;-dL~3<0b44x!mmMT+a4bC7_&I?Dbgc*P|3J zCD{5)*7xx%N^9dhz??*N=+ZOt@81T`x`mx>x^uTvR6{;9cHi5M|Bw4_gwbbqsmxJ2 z87>ptGs<#H92e#Ne(75@ssA2CH^1nbzOG??AFlfsu2Ak?@#!DsezY%WOahLHsNphI zH_AgX?&}<_TT#kQFfvaxa7Ekwdq4I^mSqmPJJX-aTEn?i+je`vs3kvSrk>J2@m^#ZiK=qhQ8x}(>Eicj`Hr%C-jq&IWahXT>5 z3m~t~A6!ATXi*d&+xnUnC=S8jye34#Z4trS7JgQIYpf2lVfOB^>v9cI%SZFY1C@P% z3B#YdphZ^t|lsdm(b&VaRs1!pa(c}_Z9L9%FH_|7#bCfa*2_+vRf-DWGb-Z;5|FKG zzbENuQGIhA1g^iSerR=GM*h_|qYt zXc`B-vK49-DSFY0AQ3-|R#!#Rcp59PY+0NJFf|><(NBUw;^(RF%_Vorw?h{`e%*6YM z5GlF^DkjmgjOwb!ey(Um8M{yC6P^qR-}szm1XzKbF&j06*eFJ}twfBj1znG70E?+y zBd9k}&!_!cr7j|F?3Mk8M-1-DOpt3A_c3{7)b}k3#h&?Mt6}x*i0{QglC&9T%1jM- z{qE4dq9H)5-%^KhA6<1kQM5e!1sM|dmtv0h>E_?tf>H4tZ1(l5vrLH-n;Ue)|K`Zr zKMgsR#|O&}Ld*S3KEKWul(kZSqx5g_YhTB<>qpNc?a#~v1s4WC}KK?#&$pU$|zI7}7 zw)H}KUOnZh6mHuJq%LIK>&4@6o9St)=n2=wY0)^^QAC|lAvW1&JKFV%3Q+&0--7tIrCoa!s1JJ`+MeJh6TW`bn8^MS8jK0SUpap6uM?;*cwNx z@-=xkjm@)^^SQE`sg!~6EL zpV@d3)cW&iDR@F~cJ|7QKYm0i$Wax!t|xa4E#9{y36}ElJYeyAX}6myw0(~603R?J;Pn2jf@_yT)hEtfT5m&}8|fd5<{uP6q(JnKyC%h0@6JH1a33KANGYraIQ?5*ioB_%Gj>ftLc0(#QoJ z7JFz?dyzWpaieQ>p*JHDdG<%8h8busNg^KkchpY#1{tn`?)Z%j#VG-h|V{ve3*xLrG*0WUS`D{&tlDO{e@H=fvu?tg8H9LZ&b z(C0JUvO4QI`de{;2-gSP5W0Ir-o5C!krEADsAkxLvs#H9)s$)o1fKOQu8(t|aZv0yFi1E!{Pq&o+U2gL`WqaZ^|EAE~ zvt;=*+ZS!y-6% ztClIw<8H1WI?#);ztQ4Y{TrXZ(qO?Cv0MCW+n9`Dvu5bgr*(_#k6Jm00;4apetR5@ zH4i&*s@;)NsC-s2>tXZhTqM?`A*XrQ=|zD_D}S6bQEQmYJE#uB=$kM}8PnLCoIe1Y z{kUEIK)jFbL|uBbkh6A0_sP}}?0Wri>NNhoSz7X8$Wu05*Br{NTTj6Et=EGTKwn1&{5|FZ z$ry%tNBfweF~N72Y>BPJuK>Kd?|v|Ow=7T=#`=A6`27^#D@$tQwv%B_2oRJ`={Fph zHJb!DA@`x9_HWUtVk7;cB>G(>pbygYkK6|l{NeQ$@a)gK#sfg0Dv39#Ha{Cziq(~Jfj z71GxqUfmtVynXa~OO_(|B!T%;{)vp&M7pRSYEN_*Ew$FnOHg&jef(Qg{Z4D?I+WgH z)%VNjcVrYQE1bS%qjzj$oi1-9kuGnGm#C zE6_yg2EH-^Vys_S`n#SnD!fW#(6O@1_ltMT?Yg`xE?xYGgN-{E`y(WM_Kd2+NH`;= z%R}d0TAljcM(#@VoetUAAqSd-WS!{TV-NdY=?1H>xuVi<)Vn8|;p-YfDN4U?GscJ3aBHAqjXgx| zuEryf@$Hg5{gAL&MnDTgk@0=0Q2n=YgHNqTIRiH|(kQqhcMGXobE{u~r!;xJ1pG(E zevz#-YuTQw_Vy$P2hq)V{VBrsn76DATTuGm@bbN1e+3q!TbBUWs>jB^SQzJKZkaO- zDJX6qGt_%2g(EfmIEvfMzU6ROo01IVIzF{P7YO3EjoubRf(`Vn>$GO)Z&CPo9=kP# zEpJob8`M-%Uq8A3EtZth%N`t@i2JKT>+#x%ZBDbZ!t!-Zq_M$l=z)h*di%Z!6e&+a z5&WGpfn6kXR03FN+>O1 zq#)uSxG#|W95^@*YXoUw!yyO~2KV&K*_Hcl&YWShF<>-M?~CDZ|A0;~`@#xy^8jQF zn@-Bm2aV_K6^ii`2FV#!5;+LKYobUJ{a(w`1E^*0^S^!-6)*7mONRSY-5BI7e$B>V zXb6AUf-)7&8SfbRhEYCk#|$V4uKm7pUXZUw=%zUoG$-tAg%uw|9?%Si{pm+5Af5LpPrU8V$_>RK%? zA)>pRa2(f_1W-NjB0>5vHT-83gr!3Y#}$xu86cj0bxvPf71ASu(HWH4n%WMUb#WiX z%A!y|mKY$S{+`%k65hYSi>&cm49i>#^S2nFzgb32h8iitS%wZoz1GmuJCVA*X)ZwM z_hNv>zWw0GD}zB9`06KK1zN#jzn8|YbY21PuaJB9V@@}Wia*h$JpJi^r(F4bgn%7+ zCz@c7qb=E#y1PYgAPu*QCyL_J|LJpxSFy$3;c~K`d7JY6OTMzr>u@hKiQB_0Q)qsBs_-&YHK2CBDR8s-PD--iE!ET8 zQCQ*XbY6Nf#E~L(R=pK$(dFcR_)dbLWqFhNPP$pH9c$}MkEo=$0&oT9qc&)<)Hpm~ z>NhB}qw2nv+<0`whOnk?uO(;I`otkxC(k-MgT=}o{rD_Z^O>$Fjhe>9v-V;7CbZ!? zoHc;A7-JJi_LgZ~!)U*MZgU{8>K1m}NWgSCGuZ_=oP$KSHUyS2P@eo26H~MZc2_T2nas+?U>X z!tP2`UeAUM&kDN5Z$x2R9SM^UEozdOx{LOIL;8j;ci4{XM2kT}M(A4UtT_d@e#v5s z+&iAwD15Z`5kF^8e;mep5PJpt>4Covnjj|?my{>Au}1zba>M>s6jF947GBaSTJ5^~ z_RN1#J&j2 zIycoqo~gWO*@xO&__#4OWAK<+D&HQwXB8B|&?c!cZfDB0Xh%M3?|3lxUjclqP1nqw z)M=oE^UGDy4^+j1WfbQ$K%Z!T3iy9Si8k;`?S)woFvI@@CmOIj9+q+Sr;)jJ;RF=p zOG?qLv4-Me(xB>dLVf*uDj5D%^SD-Mt1+NYcAG3X&)G1!Tfo@+`hE)NoA=xm*AsP1 z_pL?0CzCA}Zd;091`tl0IMVp)Z#hEsLp7B#cgR@Co-Q9NWnmH0R~}Ik#=oDcF4J?9 zNWX~%_2Uz-S}NzPVt@G82;9M7lnUP z#BQZn?k19};WMd~0E=JlP5Zh&T=xf(KD~N7!EAkhh#X3se_+De>Z9ZFmyY$F7sVin zz_r=;k1nUli*x;dw401#daRYUu8eLou|aiH7Mc|!TlySqQrz{2-6FXOKb%$6d7Q*_U?t@slkbodz=FlnaWdbzm~h^fz4 zrQFOVeP_m0V?~Q@zmrRUTg#_x{HV5Mexn~z#~aL9_vJ5G@!m)14#-mDcH-$AQ_HmK&i=X>!jETw`Imo-yOaq=)X*1cNBe2e6kJox0Hl~ z_2M$z$*uMpi+;-AsF^RZfYs1wFJ%`0G5^;z*{R4^Zewz`6 z;^jm*4JBUMb{T_9X+EgV-&o65Avi&(D~+)(UP$cew$Ez%SipvE-oWNeUhg|(bT=h| zrxAPrM8!yP=->wd@3IHr%#g;bwv$ys@H5ty&8;!ekcid${E_vC$Z$hnto9?BEB zlBbXDkC%h~xr_x)zdMNlpGNssHG2`c%X>&B`Sf-bxO~8)RK|zXOAL%hPQj^S3XG>Z zfv;^@E|uMP)_;ELG3BKZR?g*y?|j#h{3 z_tiRKexpATv14wMNB8X2D!h_>_XHn#ZGx_4pZ8faUEejWcywADe03FXh1mdLvmc+R zZTZjDYOS+z&SB!3*L|<(mKR+#92kZ$*Q$~flV zN${iISzX4qcWCX+w067V*Faj&Yq6g)CqLoE)kJRUW4gA}y2X!^F$!me02xKR=2bmP zb$E+=3v_WmyFk`sOtyu@3{+TcfFllnVSO?>>%MbmG{ti=RP=nCnK<~npB#lvM?BY@ zUZhv*5s4^j9OKRz<7=1@^+REd>7XV>oo$P)r0h!1_LhlSThqsLtEy zXYCoS1VBCEPfm6Tj3;Y=cNtgXfbL)?I7>$(;RSyGI=lBr)r|u-c>wZ6$PE0%N)iH? z4niY(`|2#i8_z|0*n0uA9k3SJYi4&Fai}E=`MGOwq(UV{7NcT*_3fp0)P|GNX7`|> z)Zao%1dVFemGcYp8*>tI+w9RC6l=79xhwQMoi%)HBDuS>k~H5AD9a%0dT)tl^u4)E zBG*r|`Ih(#SvSRJDLZ}{W^Hm|^#fo3bjsA}daiR0WDcm3@=Cv#V;T{aDL(z^HU2q) zQvGSQf6$C5YAbReWoKW;*3~UtHCEad3{F!wFzfY*T9%$?)X$d7UdZz6qMqY63%3l3 zW6PWwHkmJ6RKWEhYPcvp3!SeEwMwIWF@yz#=Z)zCsLY`!O?T_mCE=-Y%PgZ*g4X<> zmiC%g=P4+etoAfbGs%~aIqa(5LsS1rc`Cv$Wc@`#qw6budj4bDc>Jti$VYN&xVh(8 zHsQr`^afMbvBP4$C>)JvLotrfUl&!L`k~__`<#ZJKkVU4@Qn9W*zf1gXm8QKIa7j- zU!*3pOz=)zgg>6(aVV~TH$@l!fv!h0h+YiGl5~*Ss=cCUd`2WqPw1Mtj4vB9%Ov|u z_ZEA`tQr#5E#8c0u}W8W{KgTc(IJ!CTgqWPN}MuVd@||=Pq-f3)`-vG=Imq-ClUQ@q-8}*M!ielBfa?_;j%jI z`$BE{588lOtDxp;J3ydurD;Q%V`bn>l|{O^JMxpw__VElg3I|96K;CnB_tCcr8Pj> z>}vJh$%u8luF?nF^AG*^G(N^@lXH|(#$0M)US@A=c3zEt;{#pH5GDHn)|9DF%|_Y@@9PAd3tSn zx)yEwu>p^kcfwWn;Ifl>M?N%X9eP4x2a{9fSw=rd9xhEUxw6}haTVh~ zJMsV`f!YvLe3h`tiEq+JypA%*vccTkh=9|fr+~?Mm@V_qtVFM4s~vP3z6whN(QmO4 z*#;Q37ZK{P5{4$Hjb+H=koaQxDzL1AL7l(u?%L{2lE)m_hH`#0T&1w6E6McgsjB$+ zIA?Ke_osd@Wz?U+%`&85D>d>sFF>nZf9U$q;4~y~GLN7gV_(nMJ|ARVGFo^%1@eQcbp5dnuEGA~gKouQ&Dov4* zoQVQB#O)PdekSP+|KRRjcLv6RFlgLfu>i^oeKQmE-tM96yD8STmz6Fs4?pW}=29vi zg#22-yu03k5!2J1BcO8WE&Gl2*JCeK$ zVy^@AO}O+WIN%p#7>h%W!TYpJ&o== zWvek=w+ugJ&U_CiLl<$4tApgy&R|@b)V`%e8~RtkaFAt}5j;~8%Vv@q&?)HTjsledpa;yDXAvLK*v=sYUeiBE zdKx4p%jZ5AYk1(a{J#6#P0$n7R@QpXSttxrU-AjcDH_Pq-~AStNX-sew=_KJSmzp3 z(;4wl<`fL=98s~lYJFK=x_g;bS-J=GP|jCJWI9TIYT{(5yz8pJ@TUCf6Q~C7zcA$Q z?oPMRysB>UR!T@tkE|CH-Wxs39J@r5g5rx{lD%{~KVB{FP5rO<&3~T-68Qs%8Bs9u zxV-+$)q%@5xS{*qnW>b(tcFyoN1ROHK+@Y>F)4d}zhnggnXqQ7{7Sx=nXZ{$z)_b- z7b2?<96`MEsJ9u41g5JGDlDhgRr{4;Cr>i{`g9r&n8{|r-<9(}t;A3(qf+U|N_^w(3(%V?J`{!Pl7{DB#~c@}GEHGE zx=>X>0j;rosjFv}G2z{a!%!E%k&r&Vy9J*TLaKRY-B?;&Fze$OVSJZibct=NB}!1n zn6lN@WJ9|{rQO<1e~M*n+2-P9QV|Qt*y;k2YZ-{the}kJpj(>^{JR#)T1uZx`GCrz zy?duV&)hgR9MWtI`x$uKDinQ|@P7ZM*fm`L%OYHT^>hLV0@60)?w^k*%Au>C)JcUCLRa z#Xk-u`8=_WPBM~ekPH1&iGY_m8Cj*;~Bv!nbgCW?HwmoVWN&I#4YEY+pYNR?LQ>^2F=idmO z?tuS9_N#-FFsN>fHVo5zYLofkC#Bi!prJV+(A?^x9vvAfg8Y#UFZiD+kMlBUx9z3T z_!3RF-1MSFl!HNb((4oZn7#X)u(rijQ@H&LLzTjd{=N!`2Q+K}E?1uRL9pRHxK7k@ z43a+|)22Gq1IVM*&ob)$I8r);ML3*YbDH{%*tLGS)SO3G)t_dMpV=XdFBg`Lj+C;? zcd}59@SEm&!g+J+L4XCWv7_LA6VuGPeD&^p8^BCU7I-%&WVT~yr+T|3_wuG0+LKYJ&h zc9v0D&Y3sn{im~JxQ%IyZ@W!tNO-B@F{!b`+WtWqvfWp?tgPiZ^vsz7@YZ%rnRMCO5240=`qvUpV+ zN%|_*Vq86Mt~`0i%XdtIfW-77zha`6yeJMVX_ z)4=4OP*mk_#7r(?tWoHrG2-yzV(h<%Z|Xeq+OUqTzrMI^G|^XHB)b^h@R`(U+V#qa zl#TqcV)%uX&~7;w)b6-(I-fhPt&%zWo6UBE|-NZ2~fegQn zN~$2&f(Ib&*Jru)#0vD389L6=Tm@8PNq4ZfBMnt_QRrpKzhCK-9%q(Nvugt{m-G+i zK19?Lsw^8HKV96*t1#nBur!lMr_kT~>@?}n|Hd1>$?u(cQ+qA!uM}#8sQl-v&UpbY z>y+v!rn*7S)Q1}kf>jtik9yNCq#XL|#_Q<{!qc~}=@2t`-Gc4J#(~=GF6?jFem54&#R%x-5OXy)7-Ot?wwY%<*~m*A|(Ug zT>2fv-~W?=f;kv!Bp7z4WJ?#0CDIyqpEn?|v{Oe-OH|N+j;1p8>hl6g6uVv@ zjC|O~{^LSDQx4U!^=(o3_cFGp=()p>c&|r@gs!TOW6o~YiadT|UyV!lHRB(h<P6E~y_KeCs9WGhe&i$gdt=pf9_h$I#by1C(V2w(++fvl|H0M-I_mP+&&|`v+(6mQu|9JE+k+*D7`4j7jjO-+Ay(*H$pctR04HIM~icmV~V^x|VyeVy-7 z+#&VoI(4xmx6x;mEctTEYDvAmMBF9@(>=+o{{MFue5D{j2PGiL;SbR!Fk16%^{adO z|MP3>lWtYugyikoxxlG_S=$EY8r@2jrJ~JzH>0SNrn1ZZ8*%rc&l}?OIkgVwOk--g z1D;5k&njHnY-{~hjViugV34w{LksjGX-t%iNAqxqf2d(y+~QW zWWfz!s-hS%hL$RH%DPdRz+dQEoHmJwZ>2&UJH@iM!}5W9HR1M)r!8KnlW>Ffvko5l zn+6`hY2ju&CUg^VoVf|WHEb$ee5X}7PTgF@wi+#6#T>?j_u+aA$u5IRKbyfE`zS<8 zh71>ymM|X-`>HC}&!VZt7#uV-hYU3Wlxz(4sswEhXHuMwe37q9=d2Pq85;6|;VLk2 zxx|-Q4#Ob_(???%o38P>NKS^}Pi}RT__fjr2Oz>~!IE5#&bEtbp(f5^Z}#&cam9vb z8++~jN3XBY!%~=Y-8%rHL0l0H0(Wt`T1<3by5GcxvhTq`MEt0uK*%BS3L0rd(?QPk z5=}_eVL+GYz$`yxxcDn*2q1Njh(Tl8uU9xpg@g-X7tHmuA0K!bHUwt$km^wqHXEc= z|7HV-$wT;U5#{{7j6mn}=zC(Hx=7gVjUHr}T9|x5W9AVUyE;|a#4{y};S+126Sn=-}0kM)nTtboI-Ff*y zNB3_V)p&hIBu0ZAo+ydHw-~YF%%fmI`To_Q^0^TRGgV@>_||c;Df*1g^lWx3E%3OR zcKIT0dhzNfi@#axDTTw@vak53E~;}AOmNv{YCd{up}B$U-*f)&QE&1Cig~sKWEyH1 z8k*mF8Uw0wCn(GGvbD>m0_)1oW=qnRrpx_d=M4YN(OS$q6}{5m6O^qB$i-1Vp|GWy z6@q=d0)*j=HfXwPs=zTzl)3z z9o!1`D>5r$Ws6|kHBZt9vnKqu9wwm<$17(08a#JiGr-eBO=o1AfX2t+a zkn&4~iEY_s_Zxeypfk=P5L&-7GQ2YkqE-%r{r=I zH=;OOA}`inoY728JWU&}-StzUqEpPLOrwKEL$~rd6|}!!d-#K^X^cS8rm}K=jKJhU zZcmrdHhM@^-0>!zq2bg5x_k<^l{?Gal*3ynze@SX$MyWw68qs{Oi*dV5urpVTiJRQ z#afTbfBS2u2g(ZG8+O635a%EcUz*fVv_} z!mrnlmdoxgapa9zm2)yo$yVz{SxVU-A4AL1k=4q|p%y~-v2wL+eW$BJ;pEn&=uSKa zq#F#=KKaZNeutIF{5#cS?zdJhfy1lXl?npei^@P`SAkHcbN7s z;Fxk~FLy;;u=w#C=8MozKa_0Q*;oe^U!LBISP=|<^7!8Em6YcX;eHQ3ujYRMRi)_q zp3aiBTtv7i@D5%qKDj@9vdS_IQY_e?n11qPB2-oAUB?XTf40ZOYuNwOP0-v-Iu0sKF}GS|+AbH< zQa`re{?qGqPN7?VX``8FRto(fXrOQVz2CL+Zq(ZZ&+^?ri}Tk_8f1=A4uw4~5CdaY zKMP~zNh7WNMW~UShy`IEmUjAg%M^4bTPcSRHNxWpUGTk^^SH6g*3;AQPaq-rL5|U* zf`xoS8vt+A2Dq`51zZ^|0IpSJ72bSNn`@!OGGJ-hE zyZe}V&6dt@zjN!)m9i+({gVY*9pe*97H^ZlmjhIu} z#BV2YS3JpiMzd-r?^4w!%GKyp(9N)|{P06{`O!=>H_UNxU%qUq8S!eIQBbI~b^L?i zyG{vR{yBevbTe&&VN0XOYs%^0-=%h2Wi7||L*4AhIJ2EL#Ky&y_!rmz5LT;ibKzsQc*#tc*A=xbPGsXL8U(cIMW{p zjO7SOME)FlHfs#n#_PhzT#kbBma;*)%wy-7*+In?de_=+nu3$t33_cPg-lLsucP7b zhT2@*#hyQbsvOLe=1e_p*!iUxsw!;U@pV~as3dQ4|82axWcb}c^W*Jrz3deYD-R1n ziVuzpHOBvgij$F%wH`DsFpaIqIVqTIw|wfnEj2;H|K=`%{dp`+zg2$K$2waXap&35 zbH;^;$sb7G|M&eG=5;cJ79>8h*_iTii+Pmw>#ebnLWwZSx8$_IY%<`_0kN*%3_@I+ z2feS8>c|llyC}0ov1@3$RTZpb!|e^+l{xu27;I|GcPKYJ4TTX&rjI&I>wM)az7zAV zU0#LJB09umPd?IND6YExaOkN1aOr62QFjO>d^(gB;vH{~J&@fw5rK^6XG5yn6tKmv z_;pR(`Ggw>h%WB21Bwx8e|4I@bIps--6NKd$zbqe30+9VKUv~Lxt#JO)8d43&8x{> zfpmj^L)MA9)teW$_!H&_gB*O$5=j{VTkaCO^~*!H;`Ru=^97gpb;FF41>JHDd(x`7 zAA^RL4Mnciv3U_a_iOdu{;$G`#e$kr5>&a`9sWrCenejPtbMv)>yBITOHwrc=BQ1T z>j@RywjV`14g4iZntCz+!iHqp>A2@%@b6h?Rcsqci2a@aonT_b{+vc^2~6)G(=b)g z^<>D@`$w30oo;2XtMOK;ZyByWaB8!RbXVFPao0SkZFBKBF)SKMPlgnB6^WdTc62r3 z>ts; zGHF7({|>S0yPYlngX>oWt=kVpIlt3yP3k62pR1eDKKEcBW<~NrS7=xFt>e`$=Bwb? z7Z$CPE+WI;EQK#8I`c*vUFKisb#gup|GWM#f%)G`C9PU`P-)UfsG|7)dC+To#P_^u zI9HnL3V3A8<0x*QUn4d{Y@jMRN_j+?0^z!qY|rZsjc1$=-C+hr6#;KgUHwcc%(V2b z&WsMz+1I_5IXPaGCON8n%P$iyGx_MykXE)_m@jTSq*Dn#qglcq!sdO>9=gb1H2AFea=(Jx3OGGqFgpKm%O!D}kc9&7^l$7@ zL%huI+rsU=;5;0x#s@Ptr>vvn2h~o0zXg?L0N$&odNch~+ekR6uYF7QrelSYCZd2i{Bz?OP9Z_n-i+x_xdpB~Q%!+qnKzQG!Qv zAsRmrKUpHdZjeh^;ySm>dGB?`N_lK9kDFZ1*Fy`#+z(~KS|jf@Hd?Ay{X&bgBEc~@q97osX(`t6P&lx{H2lzbyL-bp_4tHA~EIHmL#8!P4Y6Ow!3^0ykg+PGmlDUen|*qkIj;8BvvrQ*jrnx^sOAq zSkM`llmLqCb_s1S)d|nzf_apx#|wnW7xNR(M?eJ)Ban*oUiet+3}Pyb=oL17*MIFC zu-5Z)&`7ZmOxu7s6LKKIys6_l&PVR*r=&bcYX*O=*TB&59#RF0z-O2%2y8(N8o$Kj z+swoI?WD%KQxB#f`$pO+fofKEOL-2Ohn`%h`3BWV|7`_gDNDa$!PN*_w6_$kvg+L9 zA>rzo8D;@jd7hNU-xMIvU06tnH^GEcQM_+_dzCOF>D{p+Auybz818OM;sAEF|b- z(q4T;<`$x|d<8M2@n7_#dt_W)v;l8FNpB|CV>w3p9v|P2L9+h8h~=I4nNinpP|J8O zgYUgjS!MlQ3-5zX-YKj3N&)%~)yiU)5548A?~`{fTcqB<)@@bdq*bW;C&+4i1NKa` z$gk`#p89Y#S*B6dml+uL>hr_t-?wflQ>iPzF%T>gXaSKW@f*K^2q)(e_|l62i4k8d zp|oWS-UTW`OSxA~fYL%r=CR1G-ugo~H^;q!Bd41&Sp9htW&P@R--aDGH~YO@x`wNm zVsXeR2}L&C0NP67y$8U%eV4-p&OJ(s=nt_z4*cA0jw?0SHWZ>Sic7fM>;DKVu|fzP z9>fpW$rVv0uztY#NHu@Y_Fiz&8~V9tSUhDq&o4{665EKSiGyA7jht2;gc&EDLP1TPtI-TxOgA3t z@y}nQl%A|Har?0SOYOJ-7k{ZUnT5HeI-QC&ih!-Dz#pu=zy)?YYLp@^8v>#4C#a|= zJvZM%&Eapll9zMXXR3_|Edvt&O@HxzuJF;Nl~UglENGGP!GdrkQhE)Gnm9{-AVbyJ z=Lxg#=P6cE=Cq*WF&@uO*eg*%4TuYm2c8*TmcQAXa-6ejGd*7{{Tjo_T*LR5;;<j5J%bWfmvR{Bi8J(&Ag z_Fe`^NQ$d|D9|){{IN~^^Hu(Cpf<4#KVLnp?`oL*kDMcqgrin;#}=CE8Pzj1pDTpgpm1v&nO{$^sW8xQUNO5FcJbS za0v-=JWNG!(fwb~gB9sSe6zoG+q%1gsfn%rZDGK*(v?QpoNIGc3Ts>h8TphGTKGJr zUA3bcuFcGQpuZ3iIAx2~ZaphIV34wd^$fv9IT#ZROMMo?oeeyZ&qADE=97%6*|A$G zOU4f*>R-LuFg(oj5b$)D!L?{T$BlH;?ZN0Q3J9Cc<}$d;?{qtW|EIe53~RFa7Jeaw z5PDO32}GLGO9-9NQL2E5bm`K&^xmWe0Ribnib{uwARTGaYiQDf6afJN>AdlI{(J9h zzaP#yU(fx8a9zpF+_PrZtXb=~W^Ojyk6lA0wRY}O52d{cI4UMX@i zH7R{PnDGIlrWqA$Em+@~&99z?|9;v!&6Ynac zyARZDU7hTE9+*bE=rnS6FIi<^zC3d&(}M{=QWFM@{~h#T_^9p+ET;h7vvW(<5%yTL ziXbwY*5_;~-E!nCwSPFdy!CUe9F zs73ty#;4B-X!PFjrIrZWOd7OF2dL;j@$k6&i8E5O?|zAO%Un|07uy%pdoRR`$J;h- zr|&<-*zzA`SGq;wh3kIqRPe3R{KYtp1qC*y89&tdIiF-q1#;LJUY;*-y0g1W9>m7F z`m)N_a`OcGVM8fcFQotk`>cfg-78^iOSr{lhFgEU&CR1uNqneVFsv7~%apz`B3q3j znkSzsIbRU7JPh{EbBmDFSS)=)jruT{B+2%hM3EO z;(&x$dYZ1mfI~VHo0{f;SM`JftnR@RuSh7Ft!!Y)y|5Am{qpju60dh~BMCC;?>GZ4 zAKYO-UzCnq|8O|^csW%xDLL)Zj6L`u)WXf=-Rg;pZL#|134`FN?X;6DBINy;{H*V2Ur6}1yfyVJo;R!2WL9y}`G$zc0ya*KJK{!oy`u)pLX*=C zeHI&HJ0(-^HJ22$W_0w`2E6m%Y!0U-q#eh;EcqN&Y~&Qh!UmPta1@z@trgs4O zbZWRKi=Q>W_?jS{K3ZOtwphv_=-=3D9+bg=GI>8g{8H($nlO$CgS6y8rMN&dN-6i! zHMT}Ao$(~0-=H$qUJ1y_rMa!UG+96SPz(0ouXiB8#sI_4DYtqC-O!JjdS*Ox>ie13 zLic}6>t(F!i3QH6c<0qT+pC`c2s3oiOJ#d7?a@!v$c)n8i{{h0WbFr;>jX8k)pgXwIz%8`gWTiT_a_wDBONv z<9oVluux;gl%%UJ;j@=dpFMe1_W`rBmUi!2g6LR%nz3S|BW_3BI~NO;A@)?O-L?ch zK0hPNtLAJi{b19c^+k!j^7YTpPf@k+{K!>K`hI97xlRTQfK{maD%;>n+$*zj3_r z{j^uxPUEwJ=)Dff@3Nh&+0*O0d674JPTJ zDWB&rtGfL6QKxIw&Y3JbFw_Z;Ro-AdQ|L#cN5w-s; zNorP76+!dfbkpI*lvDYO>Fv^;#swZe5iL=zA^RNMtCh&(n8CN!P*j z%lu3G7I?w=HST1eagJGix@~+H&uFfL+ps>H*M(EDDhk%65=~i&3Hk>8Ltd`dX~Pwj z#KJojK0gr*f|ha7+QyZz>#}m)+1eLU7Vn9g%?o?6q782OX7ctepMXK4t*L%xLF;=l zr+4>H-d-t*zJK!I%6^WpZnX^%@cQMQHCiR8Y)eY#&oUzyT*nt6V(p(F9#`_e_1QBs zg;~Z+9vhW}S&4tx97}h}PoR-0{ot1oH|Pp`n!e*>_#%B~@cs9P+kT`5LiMEq>!2VA zJUc! z$y;vv#rU_Bg!dU1`#l^TJVY8cG1`&H1voth62+-#txCR* z<<+m6L=;y&tB}p$^D3VxX!rQt^5BDts?8IJcfu9tSi`B#%knXYY0fis+9wu_zr8r# z*)%(&wfTX=JjSuhka39o<>m-Y0yqtLOnYm>M8heEyd2VyT{qTDh z#H@Y!exUK?EUB&U?UgJ2>DF-U`{~h(kSkpirmrX2^-;fvliaVKcnQ2;l3x_Zez=xd z$)O|-Atl%Z@7xvkgx2XA*+9G8-DygSOOLX>pUmD^O*yYTZQK%;*OWRoSbDN#@WF_! zv;1*;)X|Qpu3506!TTqw>Fh-*xyIWiP$6rZA8_;^!&kl9vy~!8aD4Wj2UWE9>>gD6 zc)ruWKc>uce(}gdGhMx6^?Q!`aQP3G(VW10)RggEjoi^`u_pI;M5Mx@4C-506hF0pXlIJ5GloQncY6Sy^CeSQpIbUGioL*C>jdx3HyX4D2W9jPBFK+7&x+#tFXlo@XZXN#F*{ltHQe13 zk3lxY#77@VhhU}c__9en0yhZhqqn(^GavnqzG$96Gk&5oe)l_1lne~qx@Xgff9Fe_ z79qpvkn5CbF%uluC;y%kJLaq$zVr09P5^KjDR{FypVffQ_e^9I_c13q^?vt#D!EOa zKJ<8tA%E%B!hpqfqWR?LQJ=G=j{<9|a?9Jw+JJhjM+5bUX{vidw=|J-hl*9zy=TwW zYTgTf#vE=p8}ImNWRhlzlrhWs)oX&WQ721QC+kHW(0W?nOfOS$`-$OCybeqjUxkQ= z6D;NU3C;Gd|6)6GWWLnq+Z(ybIe95`rUKb@OWYl2H_s9xWlP)R{x?9S90UT`EbnWZ!V`Xx^k`wTR)CdIe)cb@J_|xgc)u4n|641 zf2pO>c9=mx^q5>J`otn?q-(sDz9okaGGqiPAJ) z3i#E!Gg+3L>N_kpYkzUP&2MkaT3a_h9xmaZWhK_988ob(#UCukmHe)cr|MbbV7h}d zRdv9kmb8@Bgy}sr55p%lb4fUy*AhnNFxHQD_Hy!=-&0YtXmZqy{zR<) z)Am=_FWNZY9A%@up2%+Vk=97!^zQT~GUl59-diYCB*0FlZ|BXO5d=T0TJ}Azdv+YG zzF~Sd`n-jM0?%>n2sU%N^fU(i^JmzdgP3H7q0yYdZ)mu#DgCxV8VhCN$A%&B_MnD#BJ$%Eoq!c4AJZTPE4sgOv1)N}=`Mo$5=UpCbXkJd`3jPyU9N3c8k^- zzS=NP`Hn~ubdflt7l}_!9I4L80wj^#U&?i)mnkMpN@qg1NJ(l&kiLWPu%an3Io-Jwvb4+tQiw>!^C`iQ_kvlZ8;jgxNq7bkN zJ_$Yp+VlrMS!^&*-haHhAk`NmDmslOOxA$VlibIqR_uN>*A(>-zJHg+?wxXZ`EJgg zn5iLa8;nh}c3+vQ&4IrY+d%&f$9OFE@J8;B^)hX>^r^3@5@+@n?EB1p?SJtFOnn~_sDu@WIU*TnvC>%Fcec-zZE|s@chSaOT+q6o!gufWBYt-ex)rz* z_)%XHbFQ@Y)Ca>+(uW{+Klozn)u; zwg&uZZV{0rtPRD$FS!MR}dgljA#H%1;h3qMslTEgzdF=d%T2UQT7&A*$!R& zQ&Fdd&U-EkUaFk4IDN9?f-zc*BH6fwjluZ4x-sdBtSe-xH_t^F4ll-JkJ;*>K@k_@ zr)kP^^0YfTQwgB!_=X17>&P2bId*bXN4q5f@k;P*5xJ$^{Mlh2t8cyLRQ9PVO&|KM zANtNU_&qh3mY4HDljW2B(*;-Gpl`jg_`F`FuHRp-Css*UY3E7LB^p$Xt2$rJ?b7>n zu{7{p56x5O+DD|yihe+PJ(inbCvJJJT2+iET#M^nkgQAQ17Mouch|8CR)3W1pt|Ge zjJ-<1yhG>|p=u#wycOrfSlv)8^IgHNm)|*=B(|&Kida(6st3;dq8B_zE_YS)0eZCk zC_%zHg{7s((p!8?4%eaLg8-Hu_-mXUOiCu^Teud77NOtaKR{+>udmEl<;a_vZKNhF z_`ZHKx^p4YLnxmBhRZBkA>p|9bG&)3%8gb_EmVX_qO75fHT-!u?q^EulrOD44)UbL zERU2n4ERh45V>w8SPQ}fFR!zYxdV?}dZjd-5H#d(;5>kY1TTw(VZpSzd4+}nIoy?yubLFt6G;V5_eO}Y=5ySdR@kA3z<#Xv5IB@??2y7 zN{2KBUHWW3eR=Hpdsw_B)MHXpT%xSqZj@yc4hC)!`ze@D6bRU?m7m=%dX7p2xtYP0 zj*19GIEPLt)ZFCZO`G{4-z6S14SUa;ZkBfN^%t@>3-LD@tCA0L$R#Bdg!7Hxo!fdv zt@=hHct<_Ry$T(DjE*>lD+jBq(j)I3NA6MtHZ5#O1MNG*@wws{&$)@0YBsUsPt0fc zsjmj4n6x&ck=Ta~&X&JA6tq{2I$hFW6sv!>wuTzWQ*Ooa+ zlPc9lKW#MP-+d%uqRMJZoQYpRZutn~MkaBr$nr}^dnYEbg8cjp|CnP3bBX6wsyzl` zntX$1WYmL6LXuJMTm_4jvZQ==-GplitUyxIzlTJ$W?pkV%F({3_MEtC9M)etsynNm z%K5ngXb9%_DXX66B`K&vPGhxM(-o2wurrh;a)L?KmSnEQ^)JehxhT0lceP4JLC_Q8 z^j0t{1giq;Pels33mlurWP1um{hQKt$oO0&;F%dO~gHIK=?dJ%F# z)qX7D6`3PQGamQlM>0~G6~oXLTANIEMsz_UqMcxL#S?@Uc(5|NoRRNSTDy`;pj7s*<5bftVE4oO%Uq&km;I%~H+moNl&$ci+S$xL*## zE#VOmz(hdyTG;)E=Z_)xkvm-RRyBQiQ`ANjqyXBmOh^Zm;kb)`k*D=!#(1BfC?=MP z=^ldqa&Da=T==9{LYI7yT?Uu6Ox*}gsrvAIv2(6L&budN731!<6c|IN1}UTX@Hg^WCwM zsCzYRj52gw4P`0R$0%4AO#@rdP;bhB-AAf>+nboO>T%nTY=xw?F(-0PUGP%V1H9vu zvi#ur>7`y)M7|_Dxx^MCx18~$jFjFGBjbNu?#PH?@aqcRA~fmxZD_vY-m0_@FH8|*?soGdC>ZA~e*JMZSiu6}agy zeqE1suUh0#P~z@pDJ&qrC6t8=gJj9lExJw1Dk%#ztaUlt;Z{s|VeuVyt_uaUzMfd+ zwBZ8WL|nUv!s!6-CF(NFgXi!$kEk(jvY4-*3s`+3)7HL}KfsOm1fF26@|GjQ@~H?O zuN(-hzzJ|?@6~V(k8kHzmImC;w+%pBu&B+#y39`B({}=p3|IkkEn7+S zp*o{S0{^j`8qHD7-AM8U!3TH})=q^FtSQ>VnCbJ6A6ZY>PG!SvnZ|$B@Z63M(`-zb zZ+!wfUvT&f{^*Z4g>a=g{g0B|QA!NY={6zUZS6SW2BL?dCVI&zqQ!hAKq&gzt? z1=QTJvnDbH)4^S>51$Ez;i|?syX~V1x%#|!Uuo9h>aC`!2k#^Y+GdQUdSVn^)i^xB z3P7F!>2RxFcdDmn%ABwm`KR)ts;~HgPjdujw`tKysP5ItJTx7ZIz`!N0oVD!Pij)z z*%~|%98BX)sEIMo052|Xm~e^>CSCHm@qU5>*G^0l@OF(Q7ro0I8EQ3H%sRiOZ2NPI z013Lt8m=1i-BvyJyubx!Sg5epHp(rYFBJ1%2$f{jDz=E^jb#bh19WwCgnILkDOLy!B}R!+dxALRH#c?s~~%yPb&rhOFT;UAg1Jd=Z}t zYQF;k3X zc)`qoaW$OwE0HLHWiG0?v0`oVz4mJw=cQUrz6F%@xvbP=JDdxQhet{frDqemn&V`O zuLazE^o$9Rlzht7v`XP_3nv6IZ@RxIu{u~!b0-6W!s$l9eewlR^m>n*K3oI9l5{sF z+U2x2c25~=x^Qi~-v>SkhivF^){SQrgcg_NuU)zOPepmtz$JEKs6TCPpaFfI%&@PbF1OR{BC3Krot?uBKl-in2-_DIq+{;s=U#!4^b5BbLZJ>G#Xz;uTndrG3%7P8|8yum41xNciVP*^Kgc})nu+6dlC0zW&l(~ z7M&5Slsi62V^|~(3qN=&SRzL~v?bDjd!2U^E3@S|MVf8N6<$63knS)XLm8Rk3KLvL z7&Qph6Tp5{a7m7HZ3&mw}N zI^tEj0`sA=q=kj9ZbN=MYDf+lRH3sIDS@OssD{rfOei52@@tBDj)EWRlE8B`&e_1) zJlU30rddmH1G-!xsA05XZC75HgYr&h9RkL=UtmwTdCV{Jm`RVUS(g=iWWH6(Z3F#Y zq{h{~dfv1FF(;C_o*!xPX6RV~ft6V{ibUYSH#t3QvKD&^c(+i_(RaMqXJ&7h83#79gXb?8vnoHz0`x6S1!q zoI8h{teeFT=P2b(AdnV}i?`@Buwp~8{^ezZ!n2$^CAg~5lbbGpm`E!wQpDXprvRRn zNOzU3Ze}L3*DFdC9qNw_0h0B&a0VtyNsqqnJA&3>9ZsX&6?kOdX@rX&_$xeg`-0>L zFq90ja23${D5~yr?4JIy;VpKCdLzJCIaC62FFOcRKa&XL3A-FH9u4d2ir(q ziBT^xNlPvO&&gMs2A++H?~6jzO6sG)}m84u8ob5G+x>^xLbX_fD zQbw?{X-ZU}*GAG<{{vVt$qu=C7mNZj4`B362y`MEU&$25G%wS2N^m4u0^tQK;uWf>a+|S1+_}@65qfMh zA9te;v1ZaCP@}UVq@TI*5!YxLr$NV^M!vLg-L+@zvlO_%N96oOJ*ANvccs$enstk*Kb5R(>R}|fmGRndw#Nwc6J@Ew7Iio^VOB~$U3vgDwEg_?Da8hMVd2IN;VSox=%y=L9&b-rfka%aY1eRV=~&Z#&N>-@%SP`C&cOuqHE0miitGTUbY#uHas)w=UYob* z^<}Lg2`5at=T{Z#+nwF2jmafNbiVKd{@;Eq8S>eFN+spe>x8@$aRvW z+l(yc+FErRNKWlJUmbJbM@I(IY&p;86{wFvnSDcsELPyPgh}K9a|^_9+ui8m;vjp~ zeN;KEF!E80{6jDUnuKv)B*@Y6NSDhP{Y^kAw$3fDq`2BbG1vjmi%kI49gQYv$!bee z49Xma*jtfjyOe4d;_ZF2q7IOj5AeusQ)gvE7f3k}+7IOvP&*Jln~H0T*gI6mDANL1 zgHn%PH5YRTQjXOy!VvtOv_YS7N{G|R8VT!%~&ub(f-k#X<-`qEGtSyO4!! zhcn8ZLAnl=2DZKg8>bkpQ|`N_V;QpQtOa>jeJ&00mF_~U3TE@KVED(x?bicVr-cGK zh8NVQmP2~xn|4s}y`b|<{oL06ExYPdTrit$+byT^n6k20c40nBO=6Wvk3J2@mtK-FZ=9~rlpMsE<%2;-h>p8ZgiKy}PUGQY)o zuz>dnn%()3LX`lM$pSOB*eIlS7)uT;;mF0;sVYw1C`*Fd4E|u)=2a7({^qx1ul2@r zE5-?fX1=p2VTYhJWfOm*IzOoH8IF?Dk~K-baOOyZ@04pWLk``3FXaEDoIh zZXsi-r}U^SN*FFW(b1OHQ9#Ph*SQ~br7BQQ*oP51-_mZaCdj3%+un6J5<6@QeduDb#-=m^zdijFQ zVJz39Hv2a%*Zeq`bkm86(*9g5fGykHS`S4@@*B5g{mY3FBO|81EtDU1c=xvQ*W1d< zDl~33`sEKgqqG4bhYIppxb_^1tm`Tg>zl7lG#$}hE((|1?2m8Tat$8oWj!C}ny;2| zfibT4tvPpl{~QhW!)+(qWF9|T2Gw6yW3%NzZDFD}5}x78+pGE7Y1QST%I_X%1?zer z+T>kxNqPcU`pxik-iOqo&7;R^9bFozFmSd$j2vC7AY|;g< z{N-DM3+|Hzw~3^0c4z42TovuCeUoJV9Cws|AB%Z4Ds^MCCe!p?Z~Z$y<2ygrZ@~v% zQZ!7Ht2i*>^$&J#`oKhX6}mb zHz%Q<@nf90xa~^+Q!xr6NO={=kz(ga9_WV=&}uUk*vTDR$6E3CTEJX@N?>xzW%p}oF>^fNm2G!MKR_q zeYWKFQQNfIR?V5x#?to0^gN@xhKu2X=C?0p*1XG16Lx!&E3cY(+Y^d((wzU|Zo%LF z(}ps+)TKVwb^;mYfIch{I<25}Ptz+^HyHOvM@L25S9#zcn7MoiqgId}@CE#>(tLiA%qAx@1Xr zYzEtR+tO55#d^h@s{^_3H0}~lSYe}p!&4rz!(cbb?Y~VL5d)0F`ws9_*`KnstpDK8 z3WA_Ty388yCE}mF%)ap0HMx4bQJ;mIWu!)t|iKwFLz0ip?TB9a_WdXucmCW z2@lMg%8>mQaCYkd{L7cKglI}Y727v6#z-|C8fyZ-FSDa8gs2Avl<7@3?sd93l|yFk z0cVo#vU%T~=s($g(UR3D?QCbkg3SlUivP-64G4JM$~bC^zyWk%F9-kRyXY5^JN$0# z*Cwq2%dZ>}>gNG0Xci*SAXF|}tJeLsbE&eKEzZkXX7=aFtVl|N&IK4wT`L8{{w%MKr(M5~3(F8S6mLbqM)Aqb!HLuCG9UZ0}nMMs3L zI#R$uX`gkg=&&Ww9-i*5lU?>~%>!u5DT2(2(c-9Xou!3EK?|K0-7!>^0(1e$wmg{s zLyr755~{+2=AlyP4(T8Z+wJPB^1pihMxF?r_1a@DCB^jac+?UR+dU*cj3&IHCZ4T@ zunr2nMs{dR?)37%HnIEka93$88eWslD#cm%6@HTRYpQ=g4E2+v`7zgyro5f8^^VdK z&c)*pYrT-88X+plnn!H@xIR=EV-l3Z8)pV(DR~_# zEXndJ*Kjr+sfF%UpKFB%aliV#_&N0r-;i^*8%sMjIaLd1$bdJ;ka?3cPQMl3PW=!LbKE@ zQGm~*vX>t}$RV;TE5>niCHj0;`(yMeKURn@&_h$5k>lCFAZ{9rJ#nCWiO%J73R$X; zDM&GhhvhAmze3dt9xNZ!xW3nk4vKu$j7=q~;F=wtIW?sPL+T$;+-WNgyRH#CDK^FX zOD~=+hyJ&SRm0gkU&;5RNiE-5Db@hOPk(du*TK_%QM!H7SouNrmL5NxPlQDJA%rwC zl%9-5@t~D7oE|}u4YgC+5QV zF(JvR(~)#6w1NxaET}hlGHk5CC*SuWNIf#iB1XyA1QVUSz&gJMD@Sf81lKL$ zDLV8Ec6(vT8QRAas4-y1#?hge2CGtPz(Ef7A~ZF*=RG#jM>;o2?tm>m%J82K|Kb} z(O9Q`N*E0cY*QOplDQWlVt*G_FU4mULIw{;3ZMYa@}B2v&myD*x5HvMI6MRn#lHdvPvH z&7U1Y0K)YIr}mMm^gvEytBxtDl5qJxed(~o?STEA&)u6TgdX66eieQMvX2BP!=ml> zX@nV;%+U00^N{#F3m1Y@&k}Ai3Z)C@tj?)JkIzIE;MJ1=J!}otVtA`O#gkpqzIw}> z*{XmqP@_?<0wW0Cr;`@>-YUK{?~dT0ES$RTA>wPs34D4*R$C8EcYAKPl6`1!BD%$k_JAE4V0?rWgl3+HtL(<3&ERg?3s z0%0Y1b+{>3hdjfOsQ}4KWSr zxaF-Rkg(uEVp{ycr16YdJQk=h3$pQ*eP((j4wP*wamB^8c_)bj1ivXUS`hD&p3BQw*IA^YtbO8|^~9aJi)pf#z)H}+^8RO? z#qtoA0jlV?_&2}H!=*g;BMD7_>@3Nd8sT^9nSu9N!q7#UfV;C<_!gW1s?HGOkM`Bp zNA}@Hc;Wm3=3{c;Xe4+7cW{M_Wx~RdPOUFjHtgdOWI`a>{ua=@Omsk>hHoJ@Y#@B4NGzPCr&`HOaeCuyL{znV|!xdV|a__ ze%EJ_Pjiv~CbtS;F<|%yi&y!ojHl?nfAk>5!KDI(0oU%X^p54L<+jgj=H!2I_Ol?Y z^X8u)g@!)Hb|>z?&pWIw@YmkNJ`YL%1N{SmX$=3ti@{)SvHxUoQBdf-_(vLlQSpK) zidbx`|9L9@e_L>UE zPyqk8^W5HaB!= z_IBune19O;#nyHm{~uY|5dxbY@qboo^*@=2rG$zGo*jhhS&S7RcBU#)ccwpVvUs1J z^70_Vc&*9k1XpM`@UNJPRT3ZP%LYw-G!mKqX!M&F8+i)6q>cGk;v-bg8te;$uP-FH zzJGExH}&6s#{s|qUr*Fpse13t8(>?fm&_uwD26%xwY+@IF2$d3;m#lGvK2P6)Z%-q z97KgHEYFNc<6(`Pf?GPBuOlV)CGqow@q5OZ*%~SGm1yKyJ+(mH`Q|Rjns* zSle^zD`CarZI{Wi#{z0^v4<1HezTu`KP?ce#C~@A-~G$6qD@E#1O;DrJq4=D{WE{A-*612bu80(T z&3)UfA4vqP6y(vHfGy%qc{xBnE0Vf1oVk5CTU>6PuEz=Fk>YUl2r&7T{M4OlWs`@n z#z;L5w3+2cCu|*incTV!9Rg)7n(G-_T$33|yBx*+@Au36ouvIK0|+DuCgjDUNs~De zo}xB`cT|Ppn*>tCXjxo1^HZZ6KRoY)(R2BX@nTiS@gij{)qJ&k;R9@$D5trj1S{my zMlr3&b;nyUMzQ)Ok_&7V00ji+y~d$emXQ>?W5cSp*b_%BhQ&ftVfm8Y?`?-N-5>8) zS3C&qpBR}UUpW82#)I2;pevLi7@`hjx=A-QtML9gvz;WhkLAr|N>o{crkDlH+f1x_ z6Wq5GfD}MNwI^@z)rr{`@jc5J$W0FAA2JL!$XnsYVy}ya+wR3q$Nj`K9&eqTttUI4 z?#{J?)8Z^nWQ~=IowHF|Rnwj1ALA&v7^cp98C|_jA~f8;?<6mc&y65hp6Xd8zlPhL zHa6ZZ**JUaST)|@FsX%t`VE8nabQ2gm}~{?Z?Q2;BP^KU75NvVnGM4uFbNT44qV_2 zpPoIIH~ia8*Z}RnI;z7c9G^bK`Rg{IuibT7R_6BVcanwET%EG-l6OYl%QeE*i=7Y6 z+ojniD?&#Wn8*&L0`{k9X1JtVQq;Tt#;A}-zRd4epnsBP-STkCoWo)vt5ScIo_={* zo5UjXK87_IEt3T4r#SWt1kAnsk!82IBVuOebRf4cv{ADmT zKczmSLMWM-+ET#-vl!`*@6$_G_VU8W*Mp-jQGv@qqQ;hTdqye_wc6~wG<|8rbH zA0;yDL{W_D_iZXts0at{#edXZ9&T9>r;-)+Ol5zpf!AQs8J;?_9DZGA^KFs&^o!r` z#U&Wx7MrLo>9d>pJ?M{V>(Sr?7g$d?`F39Lg@fudUebHfMwBxl1zbOTYhM*?A|o|Mo|=>&{nN^Qnn*$UoYo?-+>Ov~?uNxsU4IeYj>2VXU1N3GF4?ks-yX*t+9 zKtAhn6j$zufBIHFcAI zcRYOQ@Nz_klI7(JF6~fLJqHd&|DHtqu0f=Zxn^qx#+@Ke<1Pp8N-? z87qIV^PtJ|FQ0=*>=Q(i&M;G$WFpu|F}MH#IER8@AN?81g~J}vfrT{h`|b^J<$H-R z0-r$dFUDP-s7Jm;Zca~~`DzD~#@Zd?JbR2JFJKfQKoaee_(i<5AOJ^KW_`xKuqos2 zQp+rU5SZ}vYaWuoNu@=eD3i1^xZh%rv=3v6iu38*m-5}0o_nJL-^N2S$Aan^(>g;2 z$^M#QkQHALlpcQcS~9q!nZy>7Sy>~_jI^p2o}VW+)`$DbxLf$v@ZvUkWlF{tXu5XlJ|SiP2IJ><+9$Cj{5!TaX}yXucFe z_lS}up9N=^((pb`3c8U6E1G_wbqXqS=zx_uv-bHD{Lk2aPEN($mUq1XwzmaoM>XaC zYvRDQE$*$)hPRlOZV+niEqwAtgsx~I*>UC<*h)D1DKTtR6rg<%Kc%jaQgjBd(OUT3 zdK%Pes5c%^84L_doFm-N`)rGwZ(-+1g*?ZO!X_z?{ep9H$~DSRK6`2EBFnJc^}oYV zITJ~^^H9Ez^D5>Z#j$fPakCZeJV{aK5W(qRK4@|1j)>CGJs~`s6Pp<#c7Q_xuNu@D zSIF8HZ%l=PVPyQxe~dHu*~pg?3!?A+VU@pEl4i^lH&TcYNd;PCc8Q>$;LJg;mqM*0 zlBD}>evFYF{Tea&cd#H$9+J{l6C4eMO)MC5Y#rWy@`XuMK0wfIr~SEMdHMz3)#_rj3<0_5YT5B6&QcdCjrVKPO!bwj+(4MHMdpqOTp!3Q|t2c^S}1> zsq$#`Qz9(n!I>fI_Bba9!RK~Gca_9q>Bh;VO}o8_W(kx4sGcNvP#b{{M+`n&=ym3^ zLXzhZ4pd5DpM_?wa$omI!??W#=8*g*3&Ean7{!i;Mf7 z4re4)s-pOoh&%Wx2q{DJd#BmIG>%{W@_hO7jQ0Fy1U1^HS~x~QxgEQ!#ubH;XM!h1 ztKr2(!QCSXwAmmmZrpI$9E!_<42$FVrz?b1OmZWPx}jt8yh?clBgsgqO<~F2L$=3m zN#LxP52Gl(F_922@Ot0+TefgGYfJ*%vgi8`8HV<|3~1>{(6_265bWy!6dd~$>QmoFh5AT1;4*rg`a3%mOc+3!)@O^BuEz=IkwnZOBD`VRxR;eIoI z!oV0+i5v>(%1i&KAYPunV3gI1Xf*=uY=}}OLnn{N$G1v@o0KxEB4}-TCgDdty6{%@ zEd~4I%j*fACsCl;A+koo)tXvT1*lWP-g%Wqypjw4My3lyo4x4n;gQI$a<=8<%WA^e zABj~=WzwISau{80ZL-zw?hQyV74N|_PK%stX!pAAXkrree=gBPMaTB%ghmi?F$m1R zy~{rC`KDt4@bgQ}&rb^LCnt?wIh2V5WMZj5sr47!S-^!Ue`dq%acBIduiz=T-Mh;` z?C;f7m8L>Dpeu{D-tpn2PQ#h%brz#ah;+66QqIw za^^`7P$mYtb_-sLBmNo3hx_rFe@2*FNC=E&WMyyMti3IMQ!psO^78Tq9odc{#PJ$c zs)ET1#iW#u|AY{RJDuV0n@ z_^O7d3}|SwO!AeX!6&rYON)!#qDo4(x%Kt+b={oHp*?c1@xv&s($A*#yDsBNVX45U?5P>k|k z=c5X|tcyP-AzE(1F|bISkw|B^@N z?DZN8mxlic@iVjvnlB*yt)VsKzY6$&`~thM*d1&||KI+o5Rod!#Ky)p6keqMel$8d z`gL#5`EyuPY{W-aX69?Sk&=>9Ns?K$ArlkRokxa-hF`JLuWfHPe-)panp(8{DH-{uk?jzhBWby11=|%gXYXY4ZyR zJPM=K<5Us(TLUR3M1e;XCN3Vow6rAr{_yx%{}tJc(f6gLIBerIfc|T%tEB4G(5++e;G{aIlZMvX)Ynf<^fM0qiNM AKmY&$ literal 0 HcmV?d00001 From ea020945d9a874499ad8ba646326b87ba8110207 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 16 Nov 2022 10:41:54 -0600 Subject: [PATCH 26/33] cleaned up helper function with fetch and fixed typo in define-routes.md --- docs/custom-functions/define-helpers.md | 36 +++---------------------- docs/custom-functions/define-routes.md | 2 +- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/docs/custom-functions/define-helpers.md b/docs/custom-functions/define-helpers.md index a078026f..01585aee 100644 --- a/docs/custom-functions/define-helpers.md +++ b/docs/custom-functions/define-helpers.md @@ -9,43 +9,13 @@ Helpers are functions for use within your routes. You may want to use the same h * To include the helper in your route file, you must import. With ESM, you'd use a `require` statement. -Below is code from the customValidation helper that is referenced in [Define Routes](https://harperdb.io/developers/documentation/custom-functions/define-routes/). It takes the request and the logger method from the route declaration, and makes a call to an external API to validate the headers. The API in this example is just returning a list of ToDos, but it could easily be replaced with a call to a real authentication service. +Below is code from the customValidation helper that is referenced in [Define Routes](https://harperdb.io/developers/documentation/custom-functions/define-routes/). It takes the request and the logger method from the route declaration, and makes a call to an external API to validate the headers using fetch. The API in this example is just returning a list of ToDos, but it could easily be replaced with a call to a real authentication service. ```javascript -const authRequest = (options) => { - return new Promise((resolve, reject) => { - const req = https.request(options, (res) => { - res.setEncoding('utf8'); - let responseBody = ''; - - res.on('data', (chunk) => { - responseBody += chunk; - }); - - res.on('end', () => { - resolve(JSON.parse(responseBody)); - }); - }); - - req.on('error', (err) => { - reject(err); - }); - - req.end(); - }); -}; - const customValidation = async (request,logger) => { - const options = { - hostname: 'jsonplaceholder.typicode.com', - port: 443, - path: '/todos/1', - method: 'GET', - headers: { authorization: request.headers.authorization }, - }; - - const result = await authRequest(options); + let response = await fetch('https://jsonplaceholder.typicode.com/todos/1', { headers: { authorization: request.headers.authorization } }); + let result = await response.json(); /* * throw an authentication error based on the response body or statusCode diff --git a/docs/custom-functions/define-routes.md b/docs/custom-functions/define-routes.md index 635fd58f..285632fb 100644 --- a/docs/custom-functions/define-routes.md +++ b/docs/custom-functions/define-routes.md @@ -60,7 +60,7 @@ The simple example above was just a pass-through to HarperDB- the exact same res -Below is an example of a route that uses a custom Validation hook: +Below is an example of a route that uses a custom validation hook: ```javascript const customValidation = require('../helpers/customValidation'); From c499dcf10d1c2044d1141bd7f4b2d2e5d6af41d3 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 16 Nov 2022 13:36:49 -0600 Subject: [PATCH 27/33] removed reference to cf.root --- docs/custom-functions/requirements-definitions.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/custom-functions/requirements-definitions.md b/docs/custom-functions/requirements-definitions.md index d789f8d9..9eb09634 100644 --- a/docs/custom-functions/requirements-definitions.md +++ b/docs/custom-functions/requirements-definitions.md @@ -12,8 +12,6 @@ Custom Functions is enabled by default. If you wish to manage Custom Functions d * **customFunctions.network.port** This is the port HarperDB will use to start a standalone Fastify Server dedicated to serving your Custom Functions’ routes. -* **customFunctions.root** - This is the root directory where your Custom Functions projects and their files will live. By default, it’s in your , but you can locate it anywhere--in a developer folder next to your other development projects, for example. * **customFunctions.processes** The number of processes you wish to spin up for the Custom Functions Fastify server. From b46828f2b01e6a6faa79fa1fd0ff2ebba7bfda22 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Wed, 16 Nov 2022 14:55:37 -0600 Subject: [PATCH 28/33] re-added root, removed reference to processes --- docs/custom-functions/requirements-definitions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/custom-functions/requirements-definitions.md b/docs/custom-functions/requirements-definitions.md index 9eb09634..9efaf18f 100644 --- a/docs/custom-functions/requirements-definitions.md +++ b/docs/custom-functions/requirements-definitions.md @@ -12,9 +12,9 @@ Custom Functions is enabled by default. If you wish to manage Custom Functions d * **customFunctions.network.port** This is the port HarperDB will use to start a standalone Fastify Server dedicated to serving your Custom Functions’ routes. +* **customFunctions.root** + This is the root directory where your Custom Functions projects and their files will live. By default, it’s in your \, but you can locate it anywhere--in a developer folder next to your other development projects, for example. -* **customFunctions.processes** - The number of processes you wish to spin up for the Custom Functions Fastify server. ## Project Structure **project folder** From d3c1245bd14a0b4ec8cdba483da0dd4f03d33c21 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Mon, 28 Nov 2022 14:30:34 -0600 Subject: [PATCH 29/33] adding instruction to npm init --- docs/custom-functions/create-project.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/custom-functions/create-project.md b/docs/custom-functions/create-project.md index 33e40b1a..d7ceb1ab 100644 --- a/docs/custom-functions/create-project.md +++ b/docs/custom-functions/create-project.md @@ -27,11 +27,15 @@ To manually create a project, you have three options: ``` -3. **create a project folder in your Custom Functions root directory** +3. **create a project folder in your Custom Functions root directory** and **initialize** _This requires a local installation._ ```bash > mkdir ~/hdb/custom_functions/dogs +``` + +```bash +> npm init ``` \ No newline at end of file From 5033214b21dd7725b972c493b86d95d7dfaac141 Mon Sep 17 00:00:00 2001 From: terraHDB Date: Mon, 28 Nov 2022 15:29:07 -0600 Subject: [PATCH 30/33] link to custom val import --- docs/custom-functions/define-helpers.md | 6 +++--- docs/custom-functions/define-routes.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/custom-functions/define-helpers.md b/docs/custom-functions/define-helpers.md index 01585aee..d40ffcd4 100644 --- a/docs/custom-functions/define-helpers.md +++ b/docs/custom-functions/define-helpers.md @@ -6,10 +6,10 @@ Helpers are functions for use within your routes. You may want to use the same h * To use your helpers, they must be exported from your helper file. Please use any standard export mechanisms available for your module system. We like ESM, ECMAScript Modules. Our example below exports using `module.exports`. -* To include the helper in your route file, you must import. With ESM, you'd use a `require` statement. +* You must import the helper module into the file that needs access to the exported functions. With ESM, you'd use a `require` statement. See [this example](define-routes.md#custom-preValidation-hooks) in Define Routes. -Below is code from the customValidation helper that is referenced in [Define Routes](https://harperdb.io/developers/documentation/custom-functions/define-routes/). It takes the request and the logger method from the route declaration, and makes a call to an external API to validate the headers using fetch. The API in this example is just returning a list of ToDos, but it could easily be replaced with a call to a real authentication service. +Below is code from the customValidation helper that is referenced in [Define Routes](define-routes.md). It takes the request and the logger method from the route declaration, and makes a call to an external API to validate the headers using fetch. The API in this example is just returning a list of ToDos, but it could easily be replaced with a call to a real authentication service. ```javascript @@ -29,4 +29,4 @@ const customValidation = async (request,logger) => { }; module.exports = customValidation; -``` +``` \ No newline at end of file diff --git a/docs/custom-functions/define-routes.md b/docs/custom-functions/define-routes.md index 285632fb..da5c5094 100644 --- a/docs/custom-functions/define-routes.md +++ b/docs/custom-functions/define-routes.md @@ -83,7 +83,7 @@ module.exports = async (server, { hdbCore, logger }) => { ``` -Notice we imported customValidation from the **helpers** directory. To include a helper, and to see the actual code within customValidation, see [Define Helpers](https://harperdb.io/developers/documentation/custom-functions/define-helpers/). +Notice we imported customValidation from the **helpers** directory. To include a helper, and to see the actual code within customValidation, see [Define Helpers](define-helpers.md). ## Helper Methods When declaring routes, you are given access to 2 helper methods: hdbCore and logger. From ce5646b3e0b208eeb106c352a4d35817bce7cceb Mon Sep 17 00:00:00 2001 From: terraHDB Date: Mon, 28 Nov 2022 15:59:22 -0600 Subject: [PATCH 31/33] btoa link and other adjustments from pr comments --- docs/custom-functions/define-routes.md | 2 +- docs/custom-functions/index.md | 10 +++++++--- docs/harperdb-studio/index.md | 2 +- docs/security/basic-auth.md | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/custom-functions/define-routes.md b/docs/custom-functions/define-routes.md index da5c5094..5483da85 100644 --- a/docs/custom-functions/define-routes.md +++ b/docs/custom-functions/define-routes.md @@ -8,7 +8,7 @@ Route URLs are resolved in the following manner: * [**Instance URL**]:[**Custom Functions Port**]/[**Project Name**]/[**Route URL**] -* The route below, within the **dogs** project, would be available at **http://localhost:9926/dogs**. +* The route below, within the **dogs** project, with a route of **breeds** would be available at **http://localhost:9926/dogs/breeds**. In effect, this route is just a pass-through to HarperDB. The same result could have been achieved by hitting the core HarperDB API, since it uses **hdbCore.preValidation** and **hdbCore.request**, which are defined in the “helper methods” section, below. diff --git a/docs/custom-functions/index.md b/docs/custom-functions/index.md index 2a90f1f9..49f8dd93 100644 --- a/docs/custom-functions/index.md +++ b/docs/custom-functions/index.md @@ -13,8 +13,12 @@ Custom functions are a key part of building a complete HarperDB application. It * Distribute your Custom Functions to all your HarperDB instances with a single click --- -* [Create A Project](https://harperdb.io/docs/custom-functions/create-a-project/) +* [Requirements and Definitions](requirements-definitions.md) -* [Define Routes](https://harperdb.io/docs/custom-functions/define-routes/) +* [Create A Project](create-project.md) -* [Host a Static UI](https://harperdb.io/docs/custom-functions/host-a-static-web-ui/) \ No newline at end of file +* [Define Routes](define-routes.md) + +* [Define Helpers](define-helpers.md) + +* [Host a Static UI](host-static.md) \ No newline at end of file diff --git a/docs/harperdb-studio/index.md b/docs/harperdb-studio/index.md index 6db82fe6..badc8799 100644 --- a/docs/harperdb-studio/index.md +++ b/docs/harperdb-studio/index.md @@ -5,7 +5,7 @@ HarperDB Studio is the web-based GUI for HarperDB. Studio enables you to adminis --- ## How does Studio Work? -While HarperDB Studio is web based and hosted by us, all database interactions are performed locally. The HarperDB Studio loads in your browser, at which point you login to your HarperDB instances. Credentials are stored in your browser cache and are not transmitted back to HarperDB. All database interactions are made via the HarperDB Operations API directly from your browser to your instance. +While HarperDB Studio is web based and hosted by us, all database interactions are performed on the HarperDB instance the studio is connected to. The HarperDB Studio loads in your browser, at which point you login to your HarperDB instances. Credentials are stored in your browser cache and are not transmitted back to HarperDB. All database interactions are made via the HarperDB Operations API directly from your browser to your instance. ## What type of instances can I manage? HarperDB Studio enables users to manage both HarperDB Cloud instances and privately hosted instances all from a single UI. All HarperDB instances feature identical behavior whether they are hosted by us or by you. \ No newline at end of file diff --git a/docs/security/basic-auth.md b/docs/security/basic-auth.md index 9cf0678b..b508ff96 100644 --- a/docs/security/basic-auth.md +++ b/docs/security/basic-auth.md @@ -18,6 +18,8 @@ A header is added to each HTTP request. The header key is **“Authorization” Below is a code sample from HarperDB Studio. On Line 14 you will see where we are adding the authorization header to each request. This needs to be added for each and every HTTP request for HarperDB. +_Note: This function uses btoa. Learn about [btoa here](https://developer.mozilla.org/en-US/docs/Web/API/btoa)._ + ```javascript function callHarperDB(call_object, operation, callback){ From d9f562b5342d0cca4788069c946ddb9a5464550e Mon Sep 17 00:00:00 2001 From: terraHDB Date: Tue, 29 Nov 2022 12:46:28 -0600 Subject: [PATCH 32/33] included yaml config snippet for cf --- .../requirements-definitions.md | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/custom-functions/requirements-definitions.md b/docs/custom-functions/requirements-definitions.md index 9efaf18f..fac21b00 100644 --- a/docs/custom-functions/requirements-definitions.md +++ b/docs/custom-functions/requirements-definitions.md @@ -2,19 +2,38 @@ Before you get started with Custom Functions, here’s a primer on the basic configuration and the structure of a Custom Functions Project. ## Configuration -Custom Functions is enabled by default. If you wish to manage Custom Functions directly, you will find the relevant configuration parameters in your [configuration file](https://harperdb.io/docs/reference/configuration-file/). The available settings are listed below: - - +Custom Functions are configured in the harperdb-config.yaml file located in the operations API root directory (by default this is a directory named `hdb` located in the home directory of the current user). Below is a view of the Custom Functions' section of the config YAML file, plus descriptions of important Custom Functions settings. + +```yaml +customFunctions: + enabled: true + network: + cors: true + corsAccessList: + - null + headersTimeout: 60000 + https: false + keepAliveTimeout: 5000 + port: 9926 + timeout: 120000 + nodeEnv: production + root: ~/hdb/custom_functions + tls: + certificate: ~/hdb/keys/certificate.pem + certificateAuthority: ~/hdb/keys/ca.pem + privateKey: ~/hdb/keys/privateKey.pem +``` -* **customFunctions.enabled** - A boolean value that tells HarperDB to start the Custom Functions server. Set it to **true** to enable custom functions and **false** to disable. +* **`enabled`** + A boolean value that tells HarperDB to start the Custom Functions server. Set it to **true** to enable custom functions and **false** to disable. `enabled` is `true` by default. -* **customFunctions.network.port** - This is the port HarperDB will use to start a standalone Fastify Server dedicated to serving your Custom Functions’ routes. +* **`network.port`** + This is the port HarperDB will use to start a standalone Fastify Server dedicated to serving your Custom Functions’ routes. -* **customFunctions.root** +* **`root`** This is the root directory where your Custom Functions projects and their files will live. By default, it’s in your \, but you can locate it anywhere--in a developer folder next to your other development projects, for example. +_Please visit our [configuration docs]() for a more comprehensive look at these settings._ ## Project Structure **project folder** From a98829a3d987afd7be132e31ec93908f18727d9a Mon Sep 17 00:00:00 2001 From: terraHDB Date: Tue, 29 Nov 2022 12:55:10 -0600 Subject: [PATCH 33/33] added structure user role --- docs/security/users-and-roles.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/security/users-and-roles.md b/docs/security/users-and-roles.md index 68ec76c7..b0a94a26 100644 --- a/docs/security/users-and-roles.md +++ b/docs/security/users-and-roles.md @@ -20,17 +20,23 @@ Role permissions in HarperDB are broken into two categories – permissions arou **Built-In Roles** -There are two built-in roles within HarperDB. See full breakdown of operations restricted to only super_user roles [here](https://harperdb.io/docs/security/users-roles/). -* `super_user` – this role provides full access to all operations and methods within a HarperDB instance, this can be considered the admin role. +There are three built-in roles within HarperDB. See full breakdown of operations restricted to only super_user roles [here](https://harperdb.io/docs/security/users-roles/). + +* `super_user` - This role provides full access to all operations and methods within a HarperDB instance, this can be considered the admin role. * This role provides full access to all Database Definition operations and the ability to run Database Manipulation operations across the entire database schema with no restrictions. -* `cluster_user` – this role is an internal system role type that is managed internally to allow clustered instances to communicate with one another. + +* `cluster_user` - This role is an internal system role type that is managed internally to allow clustered instances to communicate with one another. * This role is an internally managed role to facilitate communication between clustered instances. +* `structure_user` - This role provides specific access for creation and deletion of data. + + * When defining this role type you can either assign a value of true which will allow the role to create and drop schemas & tables. Alternatively the role type can be assigned a string array. The values in this array are schemas and allows the role to only create and drop tables in the designated schemas. **User-Defined Roles** + In addition to built-in roles, admins (i.e. users assigned to the super_user role) can create customized roles for other users to interact with and manipulate the data within explicitly defined tables and attributes. * Unless the user-defined role is given `super_user` permissions, permissions must be defined explicitly within the request body JSON.