diff --git a/.gitignore b/.gitignore index cf99758..a4210ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /target -Justfile \ No newline at end of file +Justfile +/commit.sh +/GO.sh +/DROP_DB.sh +/TODO.txt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 37f1db1..63c37d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,7 @@ dependencies = [ "tokio-postgres", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1384,6 +1385,7 @@ dependencies = [ "bytes", "fallible-iterator", "postgres-protocol", + "uuid", ] [[package]] @@ -1587,6 +1589,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.20" @@ -2165,6 +2173,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.2", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2216,6 +2236,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] diff --git a/README.md b/README.md index b370cdb..ae9c708 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,50 @@ Hulykvs is a simple key-value store service implemented in Rust. It uses cockroachdb as the backend and provides a simple http api for storing and retrieving key-value pairs. -## API +## API v2 +Create a key-value pair api + +```POST /api2/{workspace}/{namespace}/{key}``` +Stores request payload as the value for the given key in the given namespace. Existing keys will be overwritten. Returs 204 (NoContent) on sucesss. + + +```POST /api2/insert/{workspace}/{namespace}/{key}``` +Inserts a new key-value pair. Fails if the key already exists. Returs 204 (NoContent) on sucesss. + + +```POST /api2/update/{workspace}/{namespace}/{key}``` +Updates an existing key-value pair. Fails if the key does not exist. Returs 204 (NoContent) on sucesss. + + +```GET /api2/{workspace}/{namespace}/{key}``` +Retrieves the value for the given key in the given namespace. Returns 404 if the key does not exist. + + +```DELETE /api2/{workspace}/{namespace}/{key}``` +Deletes the key-value pair for the given key in the given namespace. Returns 404 if the key does not exist, 204 (NoContent) on success, 404 if the key does not exist. + + +```GET /api2/{workspace}/{namespace}?[prefix=]``` +Retrieves all key-value pairs in the given namespace. Optionally, a prefix can be provided to filter the results. The following structure is returned: +```json +{ + "workspace": "workspace", + "namespace": "namespace", + "count": 3, + "keys": ["key1", "key2", "keyN"] +} +``` +## API (old) +workspace = "defaultspace" + Create a key-value pair ```POST /api/{namespace}/{key}``` Stores request payload as the value for the given key in the given namespace. Existing keys will be overwritten. Returs 204 (NoContent) on sucesss. - ```GET /api/{namespace}/{key}``` Retrieves the value for the given key in the given namespace. Returns 404 if the key does not exist. - ```DELETE /api/{namespace}/{key}``` Deletes the key-value pair for the given key in the given namespace. Returns 404 if the key does not exist, 204 (NoContent) on success, 404 if the key does not exist. @@ -20,14 +53,14 @@ Deletes the key-value pair for the given key in the given namespace. Returns 404 Retrieves all key-value pairs in the given namespace. Optionally, a prefix can be provided to filter the results. The following structure is returned: ```json { - "namespace": "namespace", + "namespace": "namespace", "count": 3, "keys": ["key1", "key2", "keyN"] } ``` - + ## Running -Pre-build docker images is available at: hardcoreeng/service_hulykvs:{tag}. +Pre-build docker images is available at: hardcoreeng/service_hulykvs:{tag}. You can use the following command to run the image locally: ```bash diff --git a/commmit.sh b/commmit.sh new file mode 100755 index 0000000..1a95ae0 --- /dev/null +++ b/commmit.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +clear + +NAME="insert_update" + +git checkout -b feature/${NAME} +git add . +git commit -m "Add /api2/insert /api2/update; fix migrate; fix uuid" +git push origin feature/${NAME} diff --git a/hulykvs/hulykvs b/hulykvs/hulykvs new file mode 120000 index 0000000..20a8ba0 --- /dev/null +++ b/hulykvs/hulykvs @@ -0,0 +1 @@ +/home/work/huly.io/hulykvs/target/release/hulykvs \ No newline at end of file diff --git a/hulykvs_server/Cargo.toml b/hulykvs_server/Cargo.toml index dbfd64f..b86c4da 100644 --- a/hulykvs_server/Cargo.toml +++ b/hulykvs_server/Cargo.toml @@ -15,11 +15,14 @@ actix-cors = "0.7.1" refinery = { version = "0.8.16", features = ["tokio-postgres"] } tokio-postgres = "0.7.13" bb8 = "0.9.0" -bb8-postgres = "0.9.0" +bb8-postgres = { version = "0.9.0", features = ["with-uuid-1"] } md5 = "0.7.0" jsonwebtoken = "9.3.1" size = { version = "0.5.0", features = ["serde"] } +uuid = { version = "1.7", features = ["v4", "serde"] } +# regex = "1.10" [[bin]] name = "hulykvs" path = "src/main.rs" + diff --git a/hulykvs_server/etc/migrations/V2__workspace_uuid.sql b/hulykvs_server/etc/migrations/V2__workspace_uuid.sql new file mode 100644 index 0000000..98d7d26 --- /dev/null +++ b/hulykvs_server/etc/migrations/V2__workspace_uuid.sql @@ -0,0 +1 @@ +ALTER TABLE kvs ADD COLUMN workspace UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; diff --git a/hulykvs_server/etc/migrations/V3__workspace_uuid.sql b/hulykvs_server/etc/migrations/V3__workspace_uuid.sql new file mode 100644 index 0000000..8736265 --- /dev/null +++ b/hulykvs_server/etc/migrations/V3__workspace_uuid.sql @@ -0,0 +1 @@ +ALTER TABLE kvs ALTER PRIMARY KEY USING COLUMNS (workspace, namespace, key); diff --git a/hulykvs_server/etc/migrations/V4__workspace_uuid.sql b/hulykvs_server/etc/migrations/V4__workspace_uuid.sql new file mode 100644 index 0000000..ee13560 --- /dev/null +++ b/hulykvs_server/etc/migrations/V4__workspace_uuid.sql @@ -0,0 +1 @@ +ALTER TABLE kvs ALTER COLUMN workspace DROP DEFAULT; diff --git a/hulykvs_server/etc/migrations/V5__workspace_uuid.sql b/hulykvs_server/etc/migrations/V5__workspace_uuid.sql new file mode 100644 index 0000000..8aa25e5 --- /dev/null +++ b/hulykvs_server/etc/migrations/V5__workspace_uuid.sql @@ -0,0 +1,4 @@ +-- ALTER TABLE kvs DROP CONSTRAINT kvs_namespace_key_key; +-- DROP INDEX kvs_namespace_key_key; +DROP INDEX kvs_namespace_key_key CASCADE; + diff --git a/hulykvs_server/src/config.rs b/hulykvs_server/src/config.rs index 3d7a9e3..dc6fbcf 100644 --- a/hulykvs_server/src/config.rs +++ b/hulykvs_server/src/config.rs @@ -18,6 +18,8 @@ use std::{path::Path, sync::LazyLock}; use config::FileFormat; use serde::Deserialize; +use uuid::Uuid; + #[derive(Deserialize, Debug)] pub struct Config { pub bind_port: u16, @@ -29,6 +31,8 @@ pub struct Config { pub db_scheme: String, pub payload_size_limit: size::Size, + + pub default_workspace_uuid: Uuid, } pub static CONFIG: LazyLock = LazyLock::new(|| { diff --git a/hulykvs_server/src/config/default.toml b/hulykvs_server/src/config/default.toml index 7e33050..e88abe6 100644 --- a/hulykvs_server/src/config/default.toml +++ b/hulykvs_server/src/config/default.toml @@ -7,3 +7,6 @@ db_connection = "postgresql://root@huly.local:26257/defaultdb?sslmode=disable" db_scheme = "hulykvs" payload_size_limit = "2mb" + +default_workspace_uuid = "00000000-0000-0000-0000-000000000000" + diff --git a/hulykvs_server/src/handlers.rs b/hulykvs_server/src/handlers.rs index 340e86c..49869c1 100644 --- a/hulykvs_server/src/handlers.rs +++ b/hulykvs_server/src/handlers.rs @@ -13,6 +13,8 @@ // limitations under the License. // +use crate::config::CONFIG; + use actix_web::{ HttpResponse, error, web::{self, Data, Json, Query}, @@ -39,10 +41,10 @@ pub async fn get( let connection = pool.get().await?; let statement = r#" - select value from kvs where namespace=$1 and key=$2 + select value from kvs where workspace=$1 and namespace=$2 and key=$3 "#; - let result = connection.query(statement, &[&nsstr, &keystr]).await?; + let result = connection.query(statement, &[&CONFIG.default_workspace_uuid, &nsstr, &keystr]).await?; let response = match result.as_slice() { [] => HttpResponse::NotFound().finish(), @@ -76,16 +78,16 @@ pub async fn post( let md5 = md5::compute(&body); let statement = r#" - insert into kvs(namespace, key, md5, value) - values($1, $2, $3, $4) - on conflict(namespace, key) + insert into kvs(workspace, namespace, key, md5, value) + values($1, $2, $3, $4, $5) + on conflict(workspace, workspace, namespace, key) do update set md5=excluded.md5, value=excluded.value "#; connection - .execute(statement, &[&nsstr, &keystr, &&md5[..], &&body[..]]) + .execute(statement, &[&CONFIG.default_workspace_uuid, &nsstr, &keystr, &&md5[..], &&body[..]]) .await?; Ok(HttpResponse::NoContent().finish()) @@ -97,6 +99,7 @@ pub async fn post( }) } + pub async fn delete( path: ObjectPath, pool: Data, @@ -111,10 +114,10 @@ pub async fn delete( let connection = pool.get().await?; let statement = r#" - delete from kvs where namespace=$1 and key=$2 + delete from kvs where workspace=$1 and namespace=$2 and key=$3 "#; - let response = match connection.execute(statement, &[&nsstr, &keystr]).await? { + let response = match connection.execute(statement, &[&CONFIG.default_workspace_uuid, &nsstr, &keystr]).await? { 1 => HttpResponse::NoContent(), 0 => HttpResponse::NotFound(), _ => panic!("multiple rows deleted, unique constraint is probably violated"), @@ -157,16 +160,16 @@ pub async fn list( let response = if let Some(prefix) = &query.prefix { let pattern = format!("{}%", prefix); let statement = r#" - select key from kvs where namespace=$1 and key like $2 + select key from kvs where workspace=$1 and namespace=$2 and key like $3 "#; - connection.query(statement, &[&nsstr, &pattern]).await? + connection.query(statement, &[&CONFIG.default_workspace_uuid,&nsstr, &pattern]).await? } else { let statement = r#" - select key from kvs where namespace=$1 + select key from kvs where workspace=$1 and namespace=$2 "#; - connection.query(statement, &[&nsstr]).await? + connection.query(statement, &[&CONFIG.default_workspace_uuid,&nsstr]).await? }; let count = response.len(); diff --git a/hulykvs_server/src/handlers_v2.rs b/hulykvs_server/src/handlers_v2.rs new file mode 100644 index 0000000..aa896a9 --- /dev/null +++ b/hulykvs_server/src/handlers_v2.rs @@ -0,0 +1,332 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +use uuid::Uuid; + +use actix_web::{ + HttpResponse, error, + web::{self, Data, Json, Query}, +}; +use serde::{Deserialize, Serialize}; +use tracing::{error, trace}; + +use super::Pool; + +type BucketPath = web::Path<(String, String)>; +type ObjectPath = web::Path<(String, String, String)>; + +// ____ _____ _____ +// / ___| | ____| |_ _| +// | | _ | _| | | +// | |_| | | |___ | | +// \____| |_____| |_| +// + +pub async fn get( + path: ObjectPath, + pool: Data, +) -> Result { + let (workspace, namespace, key) = path.into_inner(); + trace!(workspace, namespace, key, "get request"); + + let wsuuid = Uuid::parse_str(workspace.as_str()) + .map_err(|e| error::ErrorBadRequest(format!("Invalid UUID in workspace: {}", e)))?; + let nsstr = namespace.as_str(); + let keystr = key.as_str(); + + async move || -> anyhow::Result { + let connection = pool.get().await?; + + let statement = r#" + select value from kvs where workspace=$1 and namespace=$2 and key=$3 + "#; + + let result = connection.query(statement, &[&wsuuid, &nsstr, &keystr]).await?; + + let response = match result.as_slice() { + [] => HttpResponse::NotFound().finish(), + [found] => HttpResponse::Ok().body(found.get::<_, Vec>("value")), + _ => panic!("multiple rows found, unique constraint is probably violated"), + }; + + Ok(response) + }() + .await + .map_err(|error| { + error!(op = "get", workspace, namespace, key, ?error, "internal error"); + error::ErrorInternalServerError("") + }) +} + +// ____ ___ ____ _____ _ +// | _ \ / _ \/ ___|_ _| _ _ _ __ ___ ___ _ __ | |_ +// | |_) | | | \___ \ | | _____ | | | | | '_ \ / __| / _ \ | '__| | __| +// | __/| |_| |___) || | |_____| | |_| | | |_) | \__ \ | __/ | | | |_ +// |_| \___/|____/ |_| \__,_| | .__/ |___/ \___| |_| \__| +// |_| +// + +pub async fn post( + path: ObjectPath, + pool: Data, + body: web::Bytes, +) -> Result { + let (workspace, namespace, key) = path.into_inner(); + trace!(workspace, namespace, key, "post request"); + + let wsuuid = Uuid::parse_str(workspace.as_str()) + .map_err(|e| error::ErrorBadRequest(format!("Invalid UUID in workspace: {}", e)))?; + let nsstr = namespace.as_str(); + let keystr = key.as_str(); + + async move || -> anyhow::Result { + let connection = pool.get().await?; + + let md5 = md5::compute(&body); + + let statement = r#" + INSERT INTO kvs(workspace, namespace, key, md5, value) + VALUES($1, $2, $3, $4, $5) + ON CONFLICT (workspace, namespace, key) + DO UPDATE SET + md5 = excluded.md5, + value = excluded.value + "#; + + connection + .execute(statement, &[&wsuuid, &nsstr, &keystr, &&md5[..], &&body[..]]) + .await?; + + Ok(HttpResponse::NoContent().finish()) + }() + .await + .map_err(|error| { + error!(op = "upsert", workspace, namespace, key, ?error, "internal error"); + error::ErrorInternalServerError("") + }) +} + + +// ____ ___ ____ _____ _ _ +// | _ \ / _ \/ ___|_ _| (_) _ __ ___ ___ _ __ | |_ +// | |_) | | | \___ \ | | _____ | | | '_ \ / __| / _ \ | '__| | __| +// | __/| |_| |___) || | |_____| | | | | | | \__ \ | __/ | | | |_ +// |_| \___/|____/ |_| |_| |_| |_| |___/ \___| |_| \__| +// + +pub async fn insert( + path: ObjectPath, + pool: Data, + body: web::Bytes, +) -> Result { + let (workspace, namespace, key) = path.into_inner(); + trace!(workspace, namespace, key, "insert request"); + + let wsuuid = Uuid::parse_str(workspace.as_str()) + .map_err(|e| error::ErrorBadRequest(format!("Invalid UUID in workspace: {}", e)))?; + let nsstr = namespace.as_str(); + let keystr = key.as_str(); + + async move || -> anyhow::Result { + let connection = pool.get().await?; + let md5 = md5::compute(&body); + + let statement = r#" + INSERT INTO kvs(workspace, namespace, key, md5, value) + VALUES($1, $2, $3, $4, $5) + "#; + + connection + .execute(statement, &[&wsuuid, &nsstr, &keystr, &&md5[..], &&body[..]]) + .await?; + + Ok(HttpResponse::Created().finish()) + }() + .await + .map_err(|error| { + error!(op = "insert", workspace, namespace, key, ?error, "internal error"); + error::ErrorInternalServerError("") + }) +} + + + +// ____ ___ ____ _____ _ _ +// | _ \ / _ \/ ___|_ _| _ _ _ __ __| | __ _ | |_ ___ +// | |_) | | | \___ \ | | _____ | | | | | '_ \ / _` | / _` | | __| / _ \ +// | __/| |_| |___) || | |_____| | |_| | | |_) | | (_| | | (_| | | |_ | __/ +// |_| \___/|____/ |_| \__,_| | .__/ \__,_| \__,_| \__| \___| +// |_| +// + +pub async fn update( + path: ObjectPath, + pool: Data, + body: web::Bytes, +) -> Result { + let (workspace, namespace, key) = path.into_inner(); + trace!(workspace, namespace, key, "update request"); + + let wsuuid = Uuid::parse_str(workspace.as_str()) + .map_err(|e| error::ErrorBadRequest(format!("Invalid UUID in workspace: {}", e)))?; + let nsstr = namespace.as_str(); + let keystr = key.as_str(); + + async move || -> anyhow::Result { + let connection = pool.get().await?; + let md5 = md5::compute(&body); + + let statement = r#" + UPDATE kvs + SET md5 = $4, value = $5 + WHERE workspace = $1 AND namespace = $2 AND key = $3 + "#; + + let updated = connection + .execute(statement, &[&wsuuid, &nsstr, &keystr, &&md5[..], &&body[..]]) + .await?; + + if updated == 0 { + return Err(anyhow::anyhow!("Not found")); + } + + Ok(HttpResponse::NoContent().finish()) + }() + .await + .map_err(|error| { + error!(op = "update", workspace, namespace, key, ?error, "internal error"); + error::ErrorInternalServerError("") + }) +} + + + + + + + + + +// ____ _____ _ _____ _____ _____ +// | _ \ | ____| | | | ____| |_ _| | ____| +// | | | | | _| | | | _| | | | _| +// | |_| | | |___ | |___ | |___ | | | |___ +// |____/ |_____| |_____| |_____| |_| |_____| +// + +pub async fn delete( + path: ObjectPath, + pool: Data, +) -> Result { + let (workspace, namespace, key) = path.into_inner(); + trace!(workspace, namespace, key, "delete request"); + + let wsuuid = Uuid::parse_str(workspace.as_str()) + .map_err(|e| error::ErrorBadRequest(format!("Invalid UUID in workspace: {}", e)))?; + let nsstr = namespace.as_str(); + let keystr = key.as_str(); + + async move || -> anyhow::Result { + let connection = pool.get().await?; + + let statement = r#" + DELETE FROM kvs WHERE workspace=$1 AND namespace=$2 AND key=$3 + "#; + + let response = match connection.execute(statement, &[&wsuuid, &nsstr, &keystr]).await? { + 1 => HttpResponse::NoContent(), + 0 => HttpResponse::NotFound(), + _ => panic!("multiple rows deleted, unique constraint is probably violated"), + }; + + Ok(response.into()) + }() + .await + .map_err(|error| { + error!(op = "delete", workspace, namespace, key, ?error, "internal error"); + error::ErrorInternalServerError("") + }) +} + +// _ ___ ____ _____ +// | | |_ _| / ___| |_ _| +// | | | | \___ \ | | +// | |___ | | ___) | | | +// |_____| |___| |____/ |_| +// + +#[derive(Deserialize)] +pub struct ListInfo { + prefix: Option, +} + +#[derive(Serialize)] +pub struct ListResponse { + workspace: String, + namespace: String, + count: usize, + keys: Vec, +} + +pub async fn list( + path: BucketPath, + pool: Data, + query: Query, +) -> Result, actix_web::error::Error> { + let (workspace, namespace) = path.into_inner(); + trace!(workspace, namespace, prefix = ?query.prefix, "list request"); + + let wsstr = workspace.as_str(); + let wsuuid = Uuid::parse_str(wsstr) + .map_err(|e| error::ErrorBadRequest(format!("Invalid UUID in workspace: {}", e)))?; + + let nsstr = namespace.as_str(); + + async move || -> anyhow::Result> { + let connection = pool.get().await?; + + let response = if let Some(prefix) = &query.prefix { + let pattern = format!("{}%", prefix); + let statement = r#" + select key from kvs where workspace=$1 and namespace=$2 and key like $3 + "#; + + connection.query(statement, &[&wsuuid, &nsstr, &pattern]).await? + } else { + let statement = r#" + select key from kvs where workspace=$1 and namespace=$2 + "#; + + connection.query(statement, &[&wsuuid, &nsstr]).await? + }; + + let count = response.len(); + + let keys = response.into_iter().map(|row| row.get(0)).collect(); + + Ok(Json(ListResponse { + keys, + count, + namespace: nsstr.to_owned(), + workspace: wsstr.to_owned(), + })) + }() + .await + .map_err(|error| { + error!(op = "list", workspace, namespace, ?error, "internal error"); + error::ErrorInternalServerError("") + }) +} diff --git a/hulykvs_server/src/main.rs b/hulykvs_server/src/main.rs index 74424d0..59a0efb 100644 --- a/hulykvs_server/src/main.rs +++ b/hulykvs_server/src/main.rs @@ -29,6 +29,7 @@ use tracing::info; mod config; mod handlers; +mod handlers_v2; mod token; use config::CONFIG; @@ -147,6 +148,16 @@ async fn main() -> anyhow::Result<()> { .route("/{bucket}/{id}", web::post().to(handlers::post)) .route("/{bucket}/{id}", web::delete().to(handlers::delete)), ) + .service( + web::scope("/api2") + .wrap(middleware::from_fn(interceptor)) + .route("/{workspace}/{bucket}", web::get().to(handlers_v2::list)) + .route("/{workspace}/{bucket}/{id}", web::get().to(handlers_v2::get)) + .route("/{workspace}/{bucket}/{id}", web::post().to(handlers_v2::post)) + .route("/insert/{workspace}/{bucket}/{id}", web::post().to(handlers_v2::insert)) + .route("/update/{workspace}/{bucket}/{id}", web::post().to(handlers_v2::update)) + .route("/{workspace}/{bucket}/{id}", web::delete().to(handlers_v2::delete)), + ) .route("/status", web::get().to(async || "ok")) }) .bind(socket)? diff --git a/scripts/claims.json b/scripts/claims.json new file mode 100644 index 0000000..0224a5f --- /dev/null +++ b/scripts/claims.json @@ -0,0 +1,7 @@ +{ + "extra": { + "service": "account" + }, + "account": "lleo", + "workspace": "bb793ec1-252e-4419-8998-7f220ababdfa" +} diff --git a/scripts/db_view.sh b/scripts/db_view.sh new file mode 100755 index 0000000..f9cae58 --- /dev/null +++ b/scripts/db_view.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +clear + +CN="postgresql://root@huly.local:26257/defaultdb?sslmode=disable" + +#psql "$CN" -c "SELECT * FROM hulykvs.kvs;" +psql "$CN" -c "SELECT workspace,namespace,key,convert_from(value,'UTF8') AS value, encode(md5,'hex') AS md5 FROM hulykvs.kvs ORDER BY namespace, key;" diff --git a/scripts/shut.sh b/scripts/shut.sh new file mode 100755 index 0000000..f774760 --- /dev/null +++ b/scripts/shut.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +clear + +NS="TESTS" +KEY="keylleo2" +VALUE="{\"name\": \"John Fox\", \"penis\": \"$(( RANDOM % 20 + 5 ))\"}" +TOKEN=$(./token.sh lleo) + +# read all (GET) +echo +echo -n "📥 GET /api/${NS} = " +curl -s -X GET "http://localhost:8094/api/${NS}" -H "Authorization: Bearer $TOKEN" + +# write (POST) +curl -s -o /dev/null -w "✅ Stored key '%s' in namespace '%s' → HTTP %s\n" \ + -X POST "http://localhost:8094/api/$NS/$KEY" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$VALUE" \ + --write-out "📥 POST(%{http_code}) /api/$NS/$KEY = $VALUE\n" + +# read (GET) +echo +echo -n "📥 GET /api/$NS/$KEY = " +curl -s -X GET "http://localhost:8094/api/$NS/$KEY" -H "Authorization: Bearer $TOKEN" +# | jq . + +# read all (GET) +echo +echo -n "📥 GET /api/${NS} = " +curl -s -X GET "http://localhost:8094/api/${NS}" -H "Authorization: Bearer $TOKEN" diff --git a/scripts/shut_v2.sh b/scripts/shut_v2.sh new file mode 100755 index 0000000..e2cbe74 --- /dev/null +++ b/scripts/shut_v2.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +clear + +#WS="Huone" +WS="00000000-0000-0000-0000-000000000010" +NS="TESTS" +KEY="AnyKey" +VALUE="{\"name\": \"Pavel\", \"penis\": \"$(( RANDOM % 20 + 5 ))\"}" +TOKEN=$(./token.sh lleo) + + +# update only test +curl -i -v -X POST "http://localhost:8094/api2/insert/${WS}/$NS/$KEY" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$VALUE" +exit + + + + + + + +#curl -i -v -X POST "http://localhost:8094/api2/${WS}/$NS/$KEY" \ +# -H "Authorization: Bearer $TOKEN" \ +# -H "Content-Type: application/json" \ +# -d "$VALUE" +# exit + + +# read all (GET) +echo +echo -n "📥 GET /api2/${WS}/${NS} = " +curl -s -X GET "http://localhost:8094/api2/${WS}/${NS}" -H "Authorization: Bearer $TOKEN" + +# write (POST) +curl -s -o /dev/null -w "✅ Stored key '%s' in namespace '%s' → HTTP %s\n" \ + -X POST "http://localhost:8094/api2/${WS}/$NS/$KEY" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$VALUE" \ + --write-out "📥 POST(%{http_code}) /api2/${WS}/$NS/$KEY = $VALUE\n" + +# read (GET) +echo +echo -n "📥 GET /api2/${WS}/$NS/$KEY = " +curl -s -X GET "http://localhost:8094/api2/${WS}/$NS/$KEY" -H "Authorization: Bearer $TOKEN" +# | jq . + +# read all (GET) +echo +echo -n "📥 GET /api2/${WS}/${NS} = " +curl -s -X GET "http://localhost:8094/api2/${WS}/${NS}" -H "Authorization: Bearer $TOKEN" + +# read all ?prefix=keyl (GET) +echo +echo -n "📥 GET /api2/${WS}/${NS}?prefix=keyl = " +curl -s -X GET "http://localhost:8094/api2/${WS}/${NS}?prefix=keyl" -H "Authorization: Bearer $TOKEN" diff --git a/scripts/token.sh b/scripts/token.sh new file mode 100755 index 0000000..1973f64 --- /dev/null +++ b/scripts/token.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +CONFIG_PATH="../hulykvs_server/src/config/default.toml" +SECRET=$(grep '^token_secret' "$CONFIG_PATH" | sed -E 's/.*=\s*"(.*)"/\1/') # " + +if [ -z "$SECRET" ]; then + echo "❌No token_secret in $CONFIG_PATH" + exit 1 +fi + +TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign claims.json) + +echo "$TOKEN" diff --git a/scripts/token.txt b/scripts/token.txt new file mode 100644 index 0000000..d5fa60e --- /dev/null +++ b/scripts/token.txt @@ -0,0 +1,41 @@ +Token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHRyYSI6eyJzZXJ2aWNlIjoiYWNjb3VudCJ9LCJhY2NvdW50IjoiYTE3ZDdjMGEtMDkwMi00NDMzLWI1M2ItMmE0NWFkMjQ4NWI4Iiwid29ya3NwYWNlIjoiYmI3OTNlYzEtMjUyZS00NDE5LTg5OTgtN2YyMjBhYmFiZGZhIn0.rvCR1fx-_T0zbHqSdk4PuZV4rbM5eyKCKpJZV4E_13E; path= + +jwt.io + +{ + "extra": { + "service": "account" + }, + "account": "a17d7c0a-0902-4433-b53b-2a45ad2485b8", + "workspace": "bb793ec1-252e-4419-8998-7f220ababdfa" +} + +https://github.com/hcengineering/hulyrs - взять отсюда jws.rs + +pub fn is_system(&self) -> bool { ... ВСЁ!!! +pub fn is_guest(&self) -> bool { ... ЗАПРЕТИТЬ!!! +pub fn is_user(&self) -> bool { ... ТОЛЬКО В СВОЕМ ВОРКСПЕЙСЕ!!! проверить по berear точно в хедерах + +1) https://www.cockroachlabs.com/docs/stable/alter-table#alter-primary-key +2) база - специальный тип uuid (128 = 16 байт) + +дефолтный uuid + + Optimistic loc + +Если в запросе в специальном хедере md5, то предварительно проверить +залочить запись +- если не совпал - статус конфликт 4... +- совпадает или не существует - апдейт + +сейчас есть "upsert" +а надо добавить: + -- строго insert + -- и строго update: без проверки md5 + +Артём Савченко - мейл +Денис Быков - календарь + +diil +deel +