42 Commits

Author SHA1 Message Date
32c2703ef2 feat: beginning of omnia
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-11 20:25:06 +01:00
f009887772 feat: remove target
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-10 19:47:14 +01:00
392ecd88db feat: remove como
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-10 19:46:38 +01:00
c718124e85 feat: with beginning of omnia
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-12-10 19:45:40 +01:00
1f88524c16 feat: add basic ci
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-22 12:31:53 +02:00
71b5a63700 feat: with como_web
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 11:23:11 +02:00
6e16fc6b2b feat: move project to crates
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 11:14:58 +02:00
381b472eca feat: with mold
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-09-08 21:40:09 +02:00
491ec81298 feat: allow dead code
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-09-05 22:05:17 +02:00
cdeefba39a feat(auth): with basic auth options
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 21:19:54 +02:00
ec483ce875 chore(fmt): run clippy fix
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 17:23:19 +02:00
1f13172ec0 fix(auth): remove rest of todos for hot path
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 16:29:07 +02:00
7a71f9b106 chore: fmt
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 16:05:27 +02:00
57d30f2129 chore(fmt): run clippy fix
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 14:09:15 +02:00
e6084a7f4e feat(auth): add authentication integration
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 14:08:40 +02:00
48d09c8ae3 refactor(auth): dyn Introspection
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 12:08:14 +02:00
f65e85dbe1 feat(auth): add file merge as base
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 01:44:54 +02:00
acde8b17e1 refactor(auth): setup convenience for OAuth
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 01:25:46 +02:00
0bb7074334 refactor(auth): move into central auth-engine setup
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 01:15:11 +02:00
5837ee0288 chore(auth): with introspection
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-20 00:23:27 +02:00
0893f285a3 chore(auth): add config and tests
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-19 23:59:33 +02:00
a2db6ca64a chore(auth): add tests
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-19 16:43:50 +02:00
7dcd3b4efe feat(auth): add base oauth client
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-19 16:24:08 +02:00
48e9d73e6d feat(auth): add base oauth client
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-19 16:15:17 +02:00
5e879b7ef2 feat(auth): add base oauth client
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-19 15:42:29 +02:00
9d064a1287 feat: with comments and abort
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 16:58:40 +02:00
258dc8779c feat: with setup ssh
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 16:57:51 +02:00
1192f366f0 Merge pull request 'Configure Renovate' (#18) from renovate/configure into main
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
Reviewed-on: #18
2023-07-27 14:47:39 +00:00
8ec89ed678 Add renovate.json
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2023-07-27 14:46:57 +00:00
0b966816a8 feat: with template
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-27 16:44:18 +02:00
3e5309e1e6 feat: update scripts to use new cuddle
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-10 14:35:03 +02:00
88c7acd439 feat: add deploy
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-04 14:55:22 +02:00
f9dcc59f3c chore: fix
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-04 14:43:36 +02:00
2604c5e301 feat: with updated deps
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-04 14:42:38 +02:00
534b2e4a23 feat: with items
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-06-04 11:02:51 +02:00
12c7c8f6ee feat: working projects and items
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-28 17:17:22 +02:00
6dc0c24443 feat: with create project
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-28 16:25:25 +02:00
c81a988061 chore: fmt all
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-28 15:43:29 +02:00
b3af0be6b5 feat: with persistent state
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-28 15:37:46 +02:00
746fb68684 feat: with persistent session state
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-28 15:34:36 +02:00
1e38b2838c feat: with zitadel login
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-28 15:05:16 +02:00
e991caef73 feat: with authorization
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-05-27 13:12:29 +02:00
64 changed files with 510 additions and 4282 deletions

View File

@@ -1,88 +0,0 @@
kind: pipeline
name: default
type: docker
steps:
- name: load_secret
image: debian:buster-slim
volumes:
- name: ssh
path: /root/.ssh/
environment:
SSH_KEY:
from_secret: gitea_id_ed25519
commands:
- mkdir -p $HOME/.ssh/
- echo "$SSH_KEY" | base64 -d > $HOME/.ssh/id_ed25519
- name: build
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
commands:
- apk add bash git
- git remote set-url origin $DRONE_GIT_SSH_URL
- cuddle_cli x setup_ssh
- cuddle_cli x start_deployment
- cuddle_cli x render_templates
- cuddle_cli x render_como_templates
- cuddle_cli x build_release
- cuddle_cli x push_release
- cuddle_cli x deploy_release
environment:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME:
from_secret: docker_username
DOCKER_PASSWORD:
from_secret: docker_password
SSH_KEY:
from_secret: gitea_id_ed25519
- name: push_tags
image: kasperhermansen/drone-semantic-release:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
commands:
- semantic-release --no-ci
environment:
DOCKER_BUILDKIT: 1
SSH_KEY:
from_secret: gitea_id_ed25519
depends_on:
- build
- name: send telegram notification
image: appleboy/drone-telegram
settings:
token:
from_secret: telegram_token
to: 2129601481
format: markdown
depends_on:
- build
- push_tags
when:
status: [failure, success]
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: ssh
temp: {}
- name: dockersock
temp: {}

11
.env
View File

@@ -1,11 +0,0 @@
POSTGRES_DB=como
POSTGRES_USER=como
POSTGRES_PASSWORD=somenotverysecurepassword
DATABASE_URL="postgres://como:somenotverysecurepassword@localhost:5432/como"
RUST_LOG=como_api=info,como_bin=info,como_core=info,como_domain=info,como_gql=info,como_infrastructure=info,sqlx=debug,tower_http=debug
TOKEN_SECRET=something
API_PORT=3001
CORS_ORIGIN=http://localhost:3000
RUN_MIGRATIONS=true
SEED=true

3
.gitignore vendored
View File

@@ -1,3 +1,2 @@
/target
target/
.cuddle/
node_modules/

View File

@@ -1,8 +0,0 @@
branches:
- "main"
plugins:
- "@semantic-release/commit-analyzer"
- "@semantic-release/release-notes-generator"
- "@semantic-release/changelog"
- "@semantic-release/git"

3276
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,3 @@
[workspace]
members = [
"como_bin",
"como_core",
"como_domain",
"como_infrastructure",
"como_gql",
"como_api",
]
members = [ "crates/*" ]
resolver = "2"

View File

@@ -1 +1,76 @@
# Cibus Backend
# Omnia
`Omnia` is a tool to provide a common platform for everything. It is a tool
build to support note-taking, handling personal relationships, project
development, todos, research, up-keep of external work, and much more.
This may seem like a large list of tasks, and it is. However, Omnia is designed
to be opinionated, and provide a minimalistic approach to each of the above.
Omnia is not a general purpose tool, text editing tool, it is designed to fit
into your existing toolstack, with its opinionated project structure, and
workflow.
Alternatives to this tool:
- Notion: with second brain templates etc.
- Obsidian: with zen garden templates etc.
Omnia is a commandline tool, which functions using a terminal ui, or pure cli
commands. Called `interactive` for the former, and `prompt` for the latter. It
is designed to work with your favorite EDITOR, to keep the scope of this project
reasonable, `Omnia` doesn't try to bundle with an editor. Instead it will
respect your `OMNIA_EDITOR`, or `EDITOR` environment variables, and launch the
files using those.
## How to use `Omnia`
To launch the fully interactive view, simply type `omnia` in your shell of
choice. This will boot up the TUI, press `?` for help and it will show a brief,
menu as well as the most common keybinds.
Following each command will be shown separately, these will be available in the
UI as well, just follow along in the menu, or use the command key `:` to open
the command palette.
### Commands
In Omnia everything is designed to use Markdown files, even the templates are
markdown files, though with some special syntax to make prompting easier. This
also means that you can open your local ~/omnia directory using your favorite
editor, as everything is just markdown files.
Projects are the cornerstone of how Omnia functions. Every navigation item is a
project, be they todo lists, research items, external sites etc. Projects can be
nested, and projects can contain pages. External apps can be configured as a
project, and will need a certain interface to be functional.
This means that when you type a command:
`omnia --help`, each subcommand will be a project, some keywords are reserved:
(todo, inbox, project, etc.), each project decides which commands are available,
and this is fully customizeable, through our plugin system. Though note that we
ship the default view with a set of preconfigured plugins:
- todo
- inbox
- projects
- areas
Typing each of the commands above will open the fully interactive tui:
`omnia todo`. Todo has a set of commands available to it: `omnia todo create` as
an example. This will open the prompt view, which will interactive ask the user
to fill out a form. These questions will also be available using commandline
flags as well.
### Views
Some of the projects are built as views, this may be a list of recent
notifications on github, apis of interest, metrics and whatnot.
## Remote first
Omnia will sync remote first in nearly all cases. The only exception is in
progress forms and whatnot. This is to keep complexity down, as well as making
sure Omnia is as easily crossplatform as possible. You should be able to use
`Omnia` from all your terminal capable devices. This sprung out of my own need
for having my notes available everywhere, without having conflicts.

View File

@@ -1,39 +0,0 @@
[package]
name = "como_api"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
como_gql = { path = "../como_gql" }
como_core = { path = "../como_core" }
como_domain = { path = "../como_domain" }
como_infrastructure = { path = "../como_infrastructure" }
async-graphql = "4.0.6"
async-graphql-axum = "*"
axum = "0.5.13"
axum-extra = { version = "*", features = ["cookie", "cookie-private"] }
axum-sessions = { version = "*" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
tokio = { version = "1.20.1", features = ["full"] }
uuid = { version = "1.1.2", features = ["v4", "fast-rng"] }
sqlx = { version = "0.6", features = [
"runtime-tokio-rustls",
"postgres",
"migrate",
"uuid",
"offline",
] }
anyhow = "1.0.60"
dotenv = "0.15.0"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
argon2 = "0.4"
rand_core = { version = "0.6", features = ["std"] }
cookie = { version = "0.16", features = ["secure", "percent-encode"] }
tower = { version = "0.4", features = ["timeout"] }
tower-http = { version = "0.3", features = ["trace", "cors"] }

View File

@@ -1,21 +0,0 @@
use async_graphql::{EmptySubscription, Schema};
use axum::{routing::get, Extension, Router};
use como_gql::{
graphql::{MutationRoot, QueryRoot},
graphql_handler, graphql_playground,
};
use como_infrastructure::register::ServiceRegister;
pub struct GraphQLController;
impl GraphQLController {
pub fn new_router(service_register: ServiceRegister) -> Router {
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(service_register)
.finish();
Router::new()
.route("/", get(graphql_playground).post(graphql_handler))
.layer(Extension(schema))
}
}

View File

@@ -1 +0,0 @@
pub mod graphql;

View File

@@ -1,2 +0,0 @@
mod controllers;
pub mod router;

View File

@@ -1,44 +0,0 @@
use anyhow::Context;
use axum::{
http::{HeaderValue, Method},
Router,
};
use como_infrastructure::register::ServiceRegister;
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use crate::controllers::graphql::GraphQLController;
pub struct Api;
impl Api {
pub async fn run_api(
port: u32,
cors_origin: &str,
service_register: ServiceRegister,
) -> anyhow::Result<()> {
let router = Router::new()
.nest(
"/graphql",
GraphQLController::new_router(service_register.clone()),
)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.layer(
CorsLayer::new()
.allow_origin(
cors_origin
.parse::<HeaderValue>()
.context("could not parse cors origin as header")?,
)
.allow_headers([axum::http::header::CONTENT_TYPE])
.allow_methods([Method::GET, Method::POST, Method::OPTIONS]),
);
axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
.serve(router.into_make_service())
.await
.context("error while starting API")?;
Ok(())
}
}

View File

@@ -1,39 +0,0 @@
[package]
name = "como_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
como_gql = { path = "../como_gql" }
como_core = { path = "../como_core" }
como_domain = { path = "../como_domain" }
como_infrastructure = { path = "../como_infrastructure" }
como_api = { path = "../como_api" }
async-graphql = "4.0.6"
async-graphql-axum = "*"
axum = "0.5.13"
axum-extra = { version = "*", features = ["cookie", "cookie-private"] }
axum-sessions = { version = "*" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
tokio = { version = "1.20.1", features = ["full"] }
uuid = { version = "1.1.2", features = ["v4", "fast-rng"] }
sqlx = { version = "0.6", features = [
"runtime-tokio-rustls",
"postgres",
"migrate",
"uuid",
"offline",
] }
anyhow = "1.0.60"
dotenv = "0.15.0"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
tower-http = { version = "0.3.4", features = ["full"] }
argon2 = "0.4"
rand_core = { version = "0.6", features = ["std"] }
cookie = { version = "0.16", features = ["secure", "percent-encode"] }
clap = { version = "3", features = ["derive", "env"] }

View File

@@ -1,22 +0,0 @@
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
#[allow(dead_code)]
#[derive(Debug)]
pub enum AppError {
WrongCredentials,
InternalServerError,
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, err_msg) = match self {
Self::WrongCredentials => (StatusCode::BAD_REQUEST, "invalid credentials"),
Self::InternalServerError => (
StatusCode::INTERNAL_SERVER_ERROR,
"something went wrong with your request",
),
};
(status, Json(json!({ "error": err_msg }))).into_response()
}
}

View File

@@ -1,41 +0,0 @@
use std::sync::Arc;
mod error;
use clap::Parser;
use anyhow::Context;
use como_api::router::Api;
use como_infrastructure::{
configs::AppConfig, database::ConnectionPoolManager, register::ServiceRegister,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing::info!("Loading dotenv");
dotenv::dotenv()?;
let config = Arc::new(AppConfig::parse());
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&config.rust_log))
.with(tracing_subscriber::fmt::layer())
.init();
let pool = ConnectionPoolManager::new_pool(&config.database_url, true).await?;
let service_register = ServiceRegister::new(pool, config.clone());
Api::run_api(
config.api_port,
&config.cors_origin,
service_register.clone(),
)
.await
.context("could not initialize API")?;
Ok(())
}

View File

@@ -1,31 +0,0 @@
[package]
name = "como_core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
como_domain = { path = "../como_domain" }
tokio = { version = "1", features = ["full"] }
axum = "0.5.1"
# utilty crates
serde = { version = "1.0.136", features = ["derive"] }
sqlx = { version = "0.5", features = [
"runtime-tokio-rustls",
"postgres",
"time",
] }
serde_json = "1.0.81"
dotenv = "0.15.0"
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"
validator = { version = "0.15", features = ["derive"] }
async-trait = "0.1"
thiserror = "1"
rust-argon2 = "1.0"
clap = { version = "3", features = ["derive", "env"] }
mockall = "0.11.1"
time = "0.2"

View File

@@ -1,18 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use como_domain::item::{
queries::{GetItemQuery, GetItemsQuery},
requests::CreateItemDto,
responses::CreatedItemDto,
ItemDto,
};
pub type DynItemService = Arc<dyn ItemService + Send + Sync>;
#[async_trait]
pub trait ItemService {
async fn add_item(&self, item: CreateItemDto) -> anyhow::Result<CreatedItemDto>;
async fn get_item(&self, query: GetItemQuery) -> anyhow::Result<ItemDto>;
async fn get_items(&self, query: GetItemsQuery) -> anyhow::Result<Vec<ItemDto>>;
}

View File

@@ -1,3 +0,0 @@
pub mod items;
pub mod projects;
pub mod users;

View File

@@ -1,15 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use como_domain::projects::{
queries::{GetProjectQuery, GetProjectsQuery},
ProjectDto,
};
pub type DynProjectService = Arc<dyn ProjectService + Send + Sync>;
#[async_trait]
pub trait ProjectService {
async fn get_project(&self, query: GetProjectQuery) -> anyhow::Result<ProjectDto>;
async fn get_projects(&self, query: GetProjectsQuery) -> anyhow::Result<Vec<ProjectDto>>;
}

View File

@@ -1,15 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
pub type DynUserService = Arc<dyn UserService + Send + Sync>;
#[async_trait]
pub trait UserService {
async fn add_user(&self, username: String, password: String) -> anyhow::Result<String>;
async fn validate_user(
&self,
username: String,
password: String,
) -> anyhow::Result<Option<String>>;
}

View File

@@ -1,13 +0,0 @@
[package]
name = "como_domain"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-graphql = { version = "4.0.6", features = ["uuid"] }
anyhow = "1.0.60"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
uuid = { version = "1.1.2", features = ["v4", "fast-rng", "serde"] }

View File

@@ -1,23 +0,0 @@
pub mod queries;
pub mod requests;
pub mod responses;
use async_graphql::{Enum, InputObject};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Enum, Copy)]
pub enum ItemState {
Created,
Done,
Archived,
Deleted,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct ItemDto {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub state: ItemState,
}

View File

@@ -1,13 +0,0 @@
use async_graphql::InputObject;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetItemQuery {
pub item_id: Uuid,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetItemsQuery {
pub user_id: Uuid,
}

View File

@@ -1,7 +0,0 @@
use async_graphql::InputObject;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct CreateItemDto {
pub name: String,
}

View File

@@ -1,3 +0,0 @@
use super::ItemDto;
pub type CreatedItemDto = ItemDto;

View File

@@ -1,3 +0,0 @@
pub mod item;
pub mod projects;
pub mod users;

View File

@@ -1,13 +0,0 @@
pub mod queries;
pub mod requests;
pub mod responses;
use async_graphql::{InputObject, SimpleObject};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject, SimpleObject)]
pub struct ProjectDto {
pub id: Uuid,
pub name: String,
}

View File

@@ -1,14 +0,0 @@
use async_graphql::InputObject;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetProjectQuery {
pub project_id: Option<Uuid>,
pub item_id: Option<Uuid>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetProjectsQuery {
pub user_id: Uuid,
}

View File

@@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct CreateProjectDto {
pub name: String,
}

View File

@@ -1,3 +0,0 @@
use super::ProjectDto;
pub type CreatedProjectDto = ProjectDto;

View File

@@ -1,12 +0,0 @@
pub mod requests;
pub mod responses;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug)]
pub struct UserDto {
pub id: Uuid,
pub username: String,
pub email: String,
}

View File

@@ -1,8 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct CreateUserDto {
pub username: String,
pub email: String,
pub password: String,
}

View File

@@ -1,3 +0,0 @@
use super::UserDto;
pub type UserCreatedDto = UserDto;

View File

@@ -1,36 +0,0 @@
[package]
name = "como_gql"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
como_core = { path = "../como_core" }
como_domain = { path = "../como_domain" }
como_infrastructure = { path = "../como_infrastructure" }
async-graphql = "4.0.6"
async-graphql-axum = "*"
axum = "0.5.13"
axum-extra = { version = "*", features = ["cookie", "cookie-private"] }
axum-sessions = { version = "*" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
tokio = { version = "1.20.1", features = ["full"] }
uuid = { version = "1.1.2", features = ["v4", "fast-rng"] }
sqlx = { version = "0.6", features = [
"runtime-tokio-rustls",
"postgres",
"migrate",
"uuid",
"offline",
] }
anyhow = "1.0.60"
dotenv = "0.15.0"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
tower-http = { version = "0.3.4", features = ["full"] }
argon2 = "0.4"
rand_core = { version = "0.6", features = ["std"] }
cookie = { version = "0.16", features = ["secure", "percent-encode"] }

View File

@@ -1,125 +0,0 @@
use async_graphql::{Context, EmptySubscription, Object, Schema};
use como_domain::{
item::{
queries::{GetItemQuery, GetItemsQuery},
requests::CreateItemDto,
},
projects::{
queries::{GetProjectQuery, GetProjectsQuery},
ProjectDto,
},
};
use como_infrastructure::register::ServiceRegister;
use crate::items::{CreatedItem, Item};
pub type ComoSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
pub struct MutationRoot;
#[Object]
impl MutationRoot {
async fn login(
&self,
ctx: &Context<'_>,
username: String,
password: String,
) -> anyhow::Result<bool> {
let service_register = ctx.data_unchecked::<ServiceRegister>();
let valid = service_register
.user_service
.validate_user(username, password)
.await?;
let returnvalid = match valid {
Some(..) => true,
None => false,
};
Ok(returnvalid)
}
async fn register(
&self,
ctx: &Context<'_>,
username: String,
password: String,
) -> anyhow::Result<String> {
let service_register = ctx.data_unchecked::<ServiceRegister>();
let user_id = service_register
.user_service
.add_user(username, password)
.await?;
Ok(user_id)
}
async fn create_item(
&self,
ctx: &Context<'_>,
item: CreateItemDto,
) -> anyhow::Result<CreatedItem> {
let services_register = ctx.data_unchecked::<ServiceRegister>();
let created_item = services_register.item_service.add_item(item).await?;
Ok(CreatedItem {
id: created_item.id,
})
}
}
pub struct QueryRoot;
#[Object]
impl QueryRoot {
// Items
async fn get_item(&self, ctx: &Context<'_>, query: GetItemQuery) -> anyhow::Result<Item> {
let item = ctx
.data_unchecked::<ServiceRegister>()
.item_service
.get_item(query)
.await?;
Ok(Item::from(item))
}
async fn get_items(
&self,
ctx: &Context<'_>,
query: GetItemsQuery,
) -> anyhow::Result<Vec<Item>> {
let items = ctx
.data_unchecked::<ServiceRegister>()
.item_service
.get_items(query)
.await?;
Ok(items.iter().map(|i| Item::from(i.clone())).collect())
}
// Projects
async fn get_project(
&self,
ctx: &Context<'_>,
query: GetProjectQuery,
) -> anyhow::Result<ProjectDto> {
ctx.data_unchecked::<ServiceRegister>()
.project_service
.get_project(query)
.await
}
async fn get_projects(
&self,
ctx: &Context<'_>,
query: GetProjectsQuery,
) -> anyhow::Result<Vec<ProjectDto>> {
ctx.data_unchecked::<ServiceRegister>()
.project_service
.get_projects(query)
.await
}
}

View File

@@ -1,76 +0,0 @@
use async_graphql::{Context, Object};
use como_domain::{
item::{queries::GetItemQuery, ItemDto, ItemState},
projects::queries::GetProjectQuery,
};
use como_infrastructure::register::ServiceRegister;
use uuid::Uuid;
use crate::projects::Project;
pub struct CreatedItem {
pub id: Uuid,
}
#[Object]
impl CreatedItem {
pub async fn item(&self, ctx: &Context<'_>) -> anyhow::Result<Item> {
let item = ctx
.data_unchecked::<ServiceRegister>()
.item_service
.get_item(GetItemQuery { item_id: self.id })
.await?;
Ok(item.into())
}
}
pub struct Item {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub state: ItemState,
}
#[Object]
impl Item {
pub async fn id(&self, _ctx: &Context<'_>) -> anyhow::Result<Uuid> {
Ok(self.id)
}
pub async fn title(&self, _ctx: &Context<'_>) -> anyhow::Result<String> {
Ok(self.title.clone())
}
pub async fn description(&self, _ctx: &Context<'_>) -> anyhow::Result<Option<String>> {
Ok(self.description.clone())
}
pub async fn state(&self, _ctx: &Context<'_>) -> anyhow::Result<ItemState> {
Ok(self.state)
}
pub async fn project(&self, ctx: &Context<'_>) -> anyhow::Result<Project> {
let project = ctx
.data_unchecked::<ServiceRegister>()
.project_service
.get_project(GetProjectQuery {
item_id: Some(self.id),
project_id: None,
})
.await?;
Ok(project.into())
}
}
impl From<ItemDto> for Item {
fn from(dto: ItemDto) -> Self {
Self {
id: dto.id,
title: dto.title,
description: dto.description,
state: dto.state,
}
}
}

View File

@@ -1,26 +0,0 @@
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
http::StatusCode,
response::{Html, IntoResponse},
};
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use graphql::ComoSchema;
pub mod graphql;
mod items;
mod projects;
pub async fn graphql_handler(
schema: Extension<ComoSchema>,
req: GraphQLRequest,
) -> Result<GraphQLResponse, StatusCode> {
let req = req.into_inner();
Ok(schema.execute(req).await.into())
}
pub async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
}

View File

@@ -1,18 +0,0 @@
use async_graphql::SimpleObject;
use como_domain::projects::ProjectDto;
use uuid::Uuid;
#[derive(SimpleObject)]
pub struct Project {
pub id: Uuid,
pub name: String,
}
impl From<ProjectDto> for Project {
fn from(dto: ProjectDto) -> Self {
Self {
id: dto.id,
name: dto.name,
}
}
}

View File

@@ -1,36 +0,0 @@
[package]
name = "como_infrastructure"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
como_core = { path = "../como_core" }
como_domain = { path = "../como_domain" }
async-graphql = "4.0.6"
async-graphql-axum = "*"
axum = "0.5.13"
axum-extra = { version = "*", features = ["cookie", "cookie-private"] }
axum-sessions = { version = "*" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
tokio = { version = "1.20.1", features = ["full"] }
uuid = { version = "1.1.2", features = ["v4", "fast-rng"] }
sqlx = { version = "0.6", features = [
"runtime-tokio-rustls",
"postgres",
"migrate",
"uuid",
"offline",
] }
anyhow = "1.0.60"
dotenv = "0.15.0"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
tower-http = { version = "0.3.4", features = ["full"] }
argon2 = "0.4"
rand_core = { version = "0.6", features = ["std"] }
cookie = { version = "0.16", features = ["secure", "percent-encode"] }
clap = { version = "3", features = ["derive", "env"] }

View File

@@ -1,8 +0,0 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username varchar not null,
password_hash varchar not null
);
CREATE unique index users_username_idx on users(username)

View File

@@ -1,17 +0,0 @@
#[derive(clap::Parser)]
pub struct AppConfig {
#[clap(long, env)]
pub database_url: String,
#[clap(long, env)]
pub rust_log: String,
#[clap(long, env)]
pub token_secret: String,
#[clap(long, env)]
pub api_port: u32,
#[clap(long, env)]
pub run_migrations: bool,
#[clap(long, env)]
pub seed: bool,
#[clap(long, env)]
pub cors_origin: String,
}

View File

@@ -1,33 +0,0 @@
use anyhow::Context;
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
use tracing::log::info;
pub type ConnectionPool = Pool<Postgres>;
pub struct ConnectionPoolManager;
impl ConnectionPoolManager {
pub async fn new_pool(
connection_string: &str,
run_migrations: bool,
) -> anyhow::Result<ConnectionPool> {
info!("initializing the database connection pool");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(connection_string)
.await
.context("error while initializing the database connection pool")?;
if run_migrations {
info!("migrations enabled");
info!("migrating database");
sqlx::migrate!()
.run(&pool)
.await
.context("error while running database migrations")?;
}
Ok(pool)
}
}

View File

@@ -1,5 +0,0 @@
pub mod configs;
pub mod database;
pub mod register;
pub mod repositories;
pub mod services;

View File

@@ -1,38 +0,0 @@
use std::sync::Arc;
use como_core::{items::DynItemService, projects::DynProjectService, users::DynUserService};
use tracing::log::info;
use crate::{
configs::AppConfig,
database::ConnectionPool,
services::{
item_service::MemoryItemService, project_service::MemoryProjectService,
user_service::DefaultUserService,
},
};
#[derive(Clone)]
pub struct ServiceRegister {
pub item_service: DynItemService,
pub project_service: DynProjectService,
pub user_service: DynUserService,
}
impl ServiceRegister {
pub fn new(pool: ConnectionPool, _config: Arc<AppConfig>) -> Self {
info!("creating services");
let item_service = Arc::new(MemoryItemService::new()) as DynItemService;
let project_service = Arc::new(MemoryProjectService::new()) as DynProjectService;
let user_service = Arc::new(DefaultUserService::new(pool)) as DynUserService;
info!("services created succesfully");
Self {
item_service,
user_service,
project_service,
}
}
}

View File

@@ -1,97 +0,0 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Context;
use axum::async_trait;
use como_core::items::ItemService;
use como_domain::item::{
queries::{GetItemQuery, GetItemsQuery},
requests::CreateItemDto,
responses::CreatedItemDto,
ItemDto,
};
use uuid::Uuid;
pub struct DefaultItemService {}
impl DefaultItemService {
pub fn new() -> Self {
Self {}
}
}
impl Default for DefaultItemService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ItemService for DefaultItemService {
async fn add_item(&self, _item: CreateItemDto) -> anyhow::Result<CreatedItemDto> {
todo!()
}
async fn get_item(&self, _query: GetItemQuery) -> anyhow::Result<ItemDto> {
todo!()
}
async fn get_items(&self, _query: GetItemsQuery) -> anyhow::Result<Vec<ItemDto>> {
todo!()
}
}
pub struct MemoryItemService {
item_store: Arc<Mutex<HashMap<String, ItemDto>>>,
}
impl MemoryItemService {
pub fn new() -> Self {
Self {
item_store: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Default for MemoryItemService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ItemService for MemoryItemService {
async fn add_item(&self, create_item: CreateItemDto) -> anyhow::Result<CreatedItemDto> {
if let Ok(mut item_store) = self.item_store.lock() {
let item = ItemDto {
id: Uuid::new_v4(),
title: create_item.name,
description: None,
state: como_domain::item::ItemState::Created,
};
item_store.insert(item.id.to_string(), item.clone());
return Ok(item);
} else {
Err(anyhow::anyhow!("could not unlock item_store"))
}
}
async fn get_item(&self, query: GetItemQuery) -> anyhow::Result<ItemDto> {
if let Ok(item_store) = self.item_store.lock() {
let item = item_store
.get(&query.item_id.to_string())
.context("could not find item")?;
return Ok(item.clone());
} else {
Err(anyhow::anyhow!("could not unlock item_store"))
}
}
async fn get_items(&self, _query: GetItemsQuery) -> anyhow::Result<Vec<ItemDto>> {
todo!()
}
}

View File

@@ -1,3 +0,0 @@
pub mod item_service;
pub mod project_service;
pub mod user_service;

View File

@@ -1,70 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::Context;
use axum::async_trait;
use como_core::projects::ProjectService;
use como_domain::projects::{
queries::{GetProjectQuery, GetProjectsQuery},
ProjectDto,
};
use tokio::sync::Mutex;
pub struct DefaultProjectService {}
impl DefaultProjectService {
pub fn new() -> Self {
Self {}
}
}
impl Default for DefaultProjectService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ProjectService for DefaultProjectService {
async fn get_project(&self, _query: GetProjectQuery) -> anyhow::Result<ProjectDto> {
todo!()
}
async fn get_projects(&self, _query: GetProjectsQuery) -> anyhow::Result<Vec<ProjectDto>> {
todo!()
}
}
pub struct MemoryProjectService {
project_store: Arc<Mutex<HashMap<String, ProjectDto>>>,
}
impl MemoryProjectService {
pub fn new() -> Self {
Self {
project_store: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Default for MemoryProjectService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ProjectService for MemoryProjectService {
async fn get_project(&self, query: GetProjectQuery) -> anyhow::Result<ProjectDto> {
let ps = self.project_store.lock().await;
if let Some(item_id) = query.item_id {
Ok(ps
.get(&item_id.to_string())
.context("could not find project")?
.clone())
} else {
Err(anyhow::anyhow!("could not find project"))
}
}
async fn get_projects(&self, _query: GetProjectsQuery) -> anyhow::Result<Vec<ProjectDto>> {
todo!()
}
}

View File

@@ -1,83 +0,0 @@
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::async_trait;
use como_core::users::UserService;
use rand_core::OsRng;
use crate::database::ConnectionPool;
pub struct DefaultUserService {
pool: ConnectionPool,
}
impl DefaultUserService {
pub fn new(pool: ConnectionPool) -> Self {
Self { pool }
}
fn hash_password(&self, password: String) -> anyhow::Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!(e))?
.to_string();
Ok(password_hash)
}
fn validate_password(&self, password: String, hashed_password: String) -> anyhow::Result<bool> {
let argon2 = Argon2::default();
let parsed_hash = PasswordHash::new(&hashed_password).map_err(|e| anyhow::anyhow!(e))?;
match argon2.verify_password(password.as_bytes(), &parsed_hash) {
Ok(..) => Ok(true),
Err(..) => Ok(false),
}
}
}
#[async_trait]
impl UserService for DefaultUserService {
async fn add_user(&self, username: String, password: String) -> anyhow::Result<String> {
let hashed_password = self.hash_password(password)?;
let rec = sqlx::query!(
r#"
INSERT INTO users (username, password_hash)
VALUES ( $1, $2 )
RETURNING id
"#,
username,
hashed_password
)
.fetch_one(&self.pool)
.await?;
Ok(rec.id.to_string())
}
async fn validate_user(
&self,
username: String,
password: String,
) -> anyhow::Result<Option<String>> {
let rec = sqlx::query!(
r#"
SELECT * from users
where username=$1
"#,
username,
)
.fetch_optional(&self.pool)
.await?;
match rec {
Some(user) => match self.validate_password(password, user.password_hash)? {
true => Ok(Some(user.id.to_string())),
false => Ok(None),
},
None => Ok(None),
}
}
}

View File

@@ -1,33 +0,0 @@
{
"query": "\n SELECT * from users\n where username=$1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password_hash",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "d3f222cf6c3d9816705426fdbed3b13cb575bb432eb1f33676c0b414e67aecaf"
}

1
crates/omnia/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

15
crates/omnia/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "omnia"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.4.11", features = ["string"] }
color-eyre = "0.6.2"
eyre = "0.6.10"
tokio = { version = "1.35.0", features = ["full"] }
tracing = { version = "0.1.40", features = ["log"] }
tracing-subscriber = { version = "0.3.18", features = ["tracing"] }
uuid = { version = "1.6.1", features = ["v4"] }

94
crates/omnia/src/main.rs Normal file
View File

@@ -0,0 +1,94 @@
use cli::CliCommand;
use inbox::services::Inbox;
#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt().init();
if let Err(e) = color_eyre::install() {
tracing::error!("failed to install color_eyre: {}", e);
}
let cmd = clap::Command::new("omnia");
let inbox = Inbox::new();
let cmd = cmd.subcommand(inbox.get_command());
let matches = cmd.get_matches();
match matches.subcommand() {
Some((name, args)) => {
tracing::debug!("executing: {}", name);
if inbox.matches_command(name) {
inbox.execute_command(args).await?;
}
}
None => {
tracing::info!("executing raw command")
}
}
Ok(())
}
pub mod cli {
use clap::{ArgMatches, Command};
pub trait CliCommand {
fn get_name(&self) -> String;
fn get_command(&self) -> Command {
Command::new(self.get_name())
}
fn matches_command(&self, name: &str) -> bool {
name == self.get_name()
}
async fn execute_command(&self, args: &ArgMatches) -> eyre::Result<()>;
}
}
pub mod inbox {
pub mod cli {}
pub mod domain {
#[derive(Debug, Clone)]
pub struct InboxItem {
pub title: String,
pub description: Option<String>,
pub project: Option<String>,
}
}
pub mod services {
use crate::cli::CliCommand;
use super::domain::*;
pub struct Inbox {}
impl Inbox {
pub fn new() -> Self {
Self {}
}
pub async fn add_item(&self, inbox_item: InboxItem) -> eyre::Result<uuid::Uuid> {
Ok(uuid::Uuid::new_v4())
}
}
impl CliCommand for Inbox {
fn get_name(&self) -> String {
"inbox".to_string()
}
async fn execute_command(&self, args: &clap::ArgMatches) -> eyre::Result<()> {
self.add_item(InboxItem {
title: "something".into(),
description: Some("description".into()),
project: Some("project".into()),
})
.await?;
Ok(())
}
}
}
}

View File

@@ -1,19 +0,0 @@
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-plan.git"
vars:
service: "como-backend"
deployments: "git@git.front.kjuulh.io:como/deployments.git"
scripts:
render_como_templates:
type: shell
local_up:
type: shell
local_down:
type: shell
run_como:
type: shell
migrate_como:
type: shell

View File

@@ -1,7 +0,0 @@
#!/bin/bash
set -e
cuddle_cli render_template --template-file $TMP/docker-compose.local_up.yml.tmpl --dest $TMP/docker-compose.local_up.yml
docker compose -f $TMP/docker-compose.local_up.yml down -v

View File

@@ -1,7 +0,0 @@
#!/bin/bash
set -e
cuddle_cli render_template --template-file $TMP/docker-compose.local_up.yml.tmpl --dest $TMP/docker-compose.local_up.yml
docker compose -f $TMP/docker-compose.local_up.yml up -d --remove-orphans --build

View File

@@ -1,6 +0,0 @@
#!/bin/bash
export $(cat .env | xargs)
cargo sqlx migrate run --source como_infrastructure/migrations --database-url=$DATABASE_URL

View File

@@ -1,10 +0,0 @@
#!/bin/bash
set -e
deploymentrepo="$TMP/deployments"
CUDDLE_FETCH_POLICY=never cuddle_cli render_template \
--template-file "$TMP/.env.example.tmpl" \
--dest "$deploymentrepo/$SERVICE/env.example"

View File

@@ -1,5 +0,0 @@
#!/bin/bash
set -e
(cd como_bin; cargo watch -x run)

View File

@@ -1,4 +0,0 @@
POSTGRES_DB=como
POSTGRES_USER=como
POSTGRES_PASSWORD=somenotverysecurepassword
DATABASE_URL="postgres://como:somenotverysecurepassword@localhost:5432/como"

View File

@@ -1,7 +0,0 @@
target/
.git/
.cuddle/
scripts/
cuddle.yaml
local.sh
README.md

View File

@@ -1,17 +0,0 @@
version: '3.7'
services:
db:
build:
context: .
dockerfile: local_up.Dockerfile
restart: always
environment:
- POSTGRES_PASSWORD=somenotverysecurepassword
ports:
- 5432:5432
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:

View File

@@ -1,8 +0,0 @@
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER como WITH PASSWORD 'somenotverysecurepassword';
CREATE DATABASE como;
GRANT ALL PRIVILEGES ON DATABASE como TO como;
EOSQL

View File

@@ -1,3 +0,0 @@
FROM postgres:14-alpine
COPY *.sh /docker-entrypoint-initdb.d/