-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* fix springrs controller issues * feat: inject tenant_id in models * change branch of rrgen * enable injection of tenant id with tenant alias * add template for controller and sql injection * fix sql schema creation order * fix: migration for default and migration for multitenancy * fix: add schema in migration * fix: get controller sql * copy dependencies' files first * feat: add support for multitenant application using row level isolation
- Loading branch information
Showing
27 changed files
with
471 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { defineConfig } from 'vite'; | ||
import react from '@vitejs/plugin-react'; | ||
|
||
// https://vitejs.dev/config/ | ||
export default defineConfig({ | ||
plugins: [react()], | ||
server: { | ||
proxy: { | ||
'/api': 'http://localhost:8080' | ||
}, | ||
}, | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
apiVersion: 0.0.1 | ||
name: spring-rs-multitenancy | ||
version: 0.0.1 | ||
description: Add multi-tenancy support to a spring-rs application | ||
keywords: | ||
- rust | ||
- spring-rs | ||
- web-framework | ||
- rest | ||
- multi-tenancy | ||
dependencies: | ||
- name: spring-rs | ||
version: 0.0.1 | ||
url: "file://../spring-rs" | ||
tags: | ||
- seaorm | ||
- rust | ||
sources: | ||
- https://github.com/dinosath/protypo | ||
maintainers: | ||
- name: Konstantinos Athanasiou | ||
email: [email protected] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
## spring-rs multitenancy generator | ||
|
||
### Overview | ||
|
||
The spring-rs multitenancy generator helps you set up a multitenancy architecture for your spring-rs based application. | ||
It supports column-based multitenancy and provides configurations to manage multiple tenants efficiently. | ||
|
||
### Key Components | ||
|
||
1. **Configuration Files**: | ||
- `values.yaml`: Contains the configuration for the multitenancy setup. | ||
- `app.toml`: Contains additional configurations for the web server and database connections. | ||
|
||
2. **Database Setup**: | ||
- `console.sql`: SQL script to set up your database schema and insert initial data. | ||
|
||
### Configuration Details | ||
|
||
#### `values.yaml` | ||
|
||
This file configures the multitenancy settings for your application. | ||
|
||
```yaml | ||
application: | ||
multitenancy: | ||
enabled: true | ||
type: "column" | ||
entity-alias: company | ||
``` | ||
- `enabled`: Enable or disable multitenancy. | ||
- `type`: Type of multitenancy. Currently supports "column". | ||
- `entity-alias`: Alias for the tenant entity. | ||
|
||
### Summary | ||
|
||
1. **Clone the Repository**: Clone the generator repository to your local machine. | ||
2. **Configure `values.yaml`**: Update the `values.yaml` file with your desired multitenancy settings. | ||
3. **Configure `app.toml`**: Update the `app.toml` file with your web server and database configurations. | ||
4. **Run Database Scripts**: Execute the SQL scripts in `console.sql` to set up your database. | ||
5. **Integrate with Your Application**: Integrate the generated configurations and scripts with your Spring application. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "Company", | ||
"type": "object", | ||
"order": 0, | ||
"properties": { | ||
"name": { | ||
"type": "string", | ||
"maxLength": 100, | ||
"x-unique": true | ||
}, | ||
"description": { | ||
"type": "string" | ||
}, | ||
"logo": { | ||
"type": "string", | ||
"format": "url" | ||
} | ||
}, | ||
"required": [ | ||
"name" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "Role", | ||
"type": "object", | ||
"order": 1, | ||
"properties": { | ||
"name": { | ||
"type": "string", | ||
"x-unique": true | ||
}, | ||
"description": { | ||
"type": "string" | ||
}, | ||
"users": { | ||
"type": "array", | ||
"items": { | ||
"$ref": "User.json" | ||
}, | ||
"x-relationship": "many-to-many" | ||
} | ||
}, | ||
"required": [ | ||
"name" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "User", | ||
"type": "object", | ||
"order": 1, | ||
"properties": { | ||
"username": { | ||
"type": "string", | ||
"x-unique": true | ||
}, | ||
"pid": { | ||
"type": "string", | ||
"format": "uuid", | ||
"readOnly": true | ||
}, | ||
"email": { | ||
"type": "string", | ||
"format": "email", | ||
"x-unique": true | ||
}, | ||
"password": { | ||
"type": "string", | ||
"writeOnly": true | ||
}, | ||
"apiKey": { | ||
"type": "string", | ||
"readOnly": true | ||
}, | ||
"firstName": { | ||
"type": "string" | ||
}, | ||
"lastName": { | ||
"type": "string" | ||
}, | ||
"resetToken": { | ||
"type": "string", | ||
"readOnly": true | ||
}, | ||
"resetSentAt": { | ||
"type": "string", | ||
"format": "date-time", | ||
"readOnly": true | ||
}, | ||
"emailVerificationToken": { | ||
"type": "string", | ||
"readOnly": true | ||
}, | ||
"emailVerificationSentAt": { | ||
"type": "string", | ||
"format": "date-time", | ||
"readOnly": true | ||
}, | ||
"emailVerifiedAt": { | ||
"type": "string", | ||
"format": "date-time", | ||
"readOnly": true | ||
}, | ||
"roles": { | ||
"type": "array", | ||
"items": { | ||
"$ref": "Role.json" | ||
}, | ||
"x-relationship": "many-to-many" | ||
} | ||
}, | ||
"required": [ | ||
"username", | ||
"pid", | ||
"password", | ||
"apiKey" | ||
] | ||
} |
90 changes: 90 additions & 0 deletions
90
generators/spring-rs-multitenancy/files/src/controllers/utils.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
use sea_orm::{ColumnTrait, DatabaseTransaction, Statement, TransactionTrait, ConnectionTrait}; | ||
use sea_orm::QueryFilter; | ||
use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr, EntityTrait, ModelTrait, Value}; | ||
use serde::Deserialize; | ||
use std::collections::HashSet; | ||
use spring_web::error::KnownWebError; | ||
|
||
#[derive(Deserialize, Debug)] | ||
pub struct ListParams { | ||
#[serde(default, deserialize_with = "deserialize_ids")] | ||
pub ids: Option<Vec<i64>>, | ||
pub offset: Option<i64>, | ||
pub limit: Option<i64>, | ||
} | ||
|
||
pub fn deserialize_ids<'de, D>(deserializer: D) -> Result<Option<Vec<i64>>, D::Error> | ||
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
let s: Option<String> = Option::deserialize(deserializer)?; | ||
match s { | ||
Some(s) => { | ||
let ids = s.split(',') | ||
.map(str::parse::<i64>) | ||
.collect::<Result<Vec<_>, _>>() | ||
.map_err(serde::de::Error::custom)?; | ||
Ok(Some(ids)) | ||
} | ||
None => Ok(None), | ||
} | ||
} | ||
|
||
pub async fn update_relation_with_diff<E, A>( | ||
db: &DatabaseTransaction, | ||
id: i32, | ||
new_ids: HashSet<i32>, | ||
entity: E, | ||
id_column: E::Column, | ||
relation_id_column: E::Column, | ||
active_model_fn: impl Fn(i32, i32) -> A, | ||
) -> Result<(), DbErr> | ||
where | ||
E: EntityTrait, | ||
E::Model: Sync, | ||
E::Model: ModelTrait, | ||
A: ActiveModelTrait<Entity = E>, | ||
{ | ||
let existing_lists: HashSet<i32> = E::find() | ||
.filter(id_column.eq(id)) | ||
.all(db) | ||
.await? | ||
.iter() | ||
.filter_map(|model| { | ||
match model.get(relation_id_column) { | ||
Value::Int(Some(id)) => Some(id), | ||
_ => None, | ||
} | ||
}) | ||
.collect::<HashSet<i32>>(); | ||
|
||
let lists_to_insert: Vec<A> = new_ids.difference(&existing_lists) | ||
.map(|&related_id| active_model_fn(id, related_id)) | ||
.collect(); | ||
|
||
let lists_to_delete: Vec<i32> = existing_lists.difference(&existing_lists) | ||
.copied() | ||
.collect(); | ||
|
||
if !lists_to_insert.is_empty() { | ||
E::insert_many(lists_to_insert).exec(db).await?; | ||
} | ||
|
||
if !lists_to_delete.is_empty() { | ||
E::delete_many() | ||
.filter(id_column.eq(id)) | ||
.filter(relation_id_column.is_in(lists_to_delete)) | ||
.exec(db) | ||
.await?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
pub async fn set_tenant(db: &DatabaseConnection, tenant_id: i32) -> Result<DatabaseTransaction, KnownWebError> { | ||
let txn = db.begin().await.map_err(|_| KnownWebError::internal_server_error("cannot create transaction"))?; | ||
let query_raw = format!("SET app.current_company = '{}';", tenant_id); | ||
let query = Statement::from_string(sea_orm::DatabaseBackend::Postgres, query_raw); | ||
txn.execute(query).await.map_err(|_| KnownWebError::internal_server_error("cannot set app.current_company"))?; | ||
|
||
Ok(txn) | ||
} |
38 changes: 38 additions & 0 deletions
38
generators/spring-rs-multitenancy/templates/02_add_multitenancy_support.sql.jinja
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{% set file_name = '02_add_multitenancy_support.sql' -%} | ||
{% if 'multitenancy' in values.application and 'enabled' in values.application.multitenancy and values.application.multitenancy.enabled == true and values.application.multitenancy.type == 'column' -%} | ||
{% set tenant_name = (values.application.multitenancy.alias | default('company')) | snake_case -%} | ||
{% set schema = values.application.name | default('app') -%} | ||
to: {{ values.outputFolder }}/migrations/{{ file_name }} | ||
message: "Sql file `{{ file_name }}` was added successfully." | ||
injections: | ||
- into: {{ values.outputFolder }}/config/app.toml | ||
replace: "postgres:xudjf23adj213" | ||
content: "tenant_user:password" | ||
- into: {{ values.outputFolder }}/config/app.toml | ||
after: "localhost:5432" | ||
inline: true | ||
content: "/postgres" | ||
|
||
--- | ||
{% import "_macros.jinja" as macros -%} | ||
CREATE OR REPLACE | ||
FUNCTION get_current_{{ tenant_name }}() RETURNS INTEGER AS $$ SELECT | ||
NULLIF(current_setting('app.current_{{ tenant_name }}',TRUE),'')::INTEGER | ||
$$ LANGUAGE SQL SECURITY DEFINER; | ||
|
||
create role tenant_user noinherit login password 'password'; | ||
GRANT USAGE ON SCHEMA {{ schema }} TO tenant_user; | ||
GRANT all privileges on all tables in schema {{ schema }} to tenant_user; | ||
ALTER DEFAULT PRIVILEGES IN SCHEMA {{ schema }} GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO tenant_user; | ||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA {{ schema }} TO tenant_user; | ||
|
||
{% for entity_name,entity in entities | items | rejectattr("0","eq", tenant_name) -%} | ||
{% set table = entity.title | plural | snake_case -%} | ||
ALTER TABLE {{ schema }}.{{ table }} ENABLE ROW LEVEL SECURITY; | ||
ALTER TABLE {{ schema }}.{{ table }} ADD COLUMN "{{ tenant_name }}_id" INTEGER NOT NULL DEFAULT get_current_company(); | ||
ALTER TABLE {{ schema }}.{{ table }} ADD FOREIGN KEY ("{{ tenant_name }}_id") REFERENCES {{ schema }}.{{ tenant_name | plural | snake_case }}(id) ; | ||
CREATE POLICY tenant_isolation_policy ON {{ schema }}.{{ table }} USING ({{ tenant_name }}_id = get_current_{{ tenant_name }}() ); | ||
|
||
|
||
{% endfor -%} | ||
{% endif -%} |
Oops, something went wrong.