gaard-api 0.2.12__tar.gz → 0.2.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gaard_api-0.2.12/src/gaard_api.egg-info → gaard_api-0.2.14}/PKG-INFO +1 -1
- {gaard_api-0.2.12 → gaard_api-0.2.14}/pyproject.toml +1 -1
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database_initial.sql +14 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database_updates.sql +13 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/models.py +25 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/services.py +1 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/main.js +13 -12
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/admin.py +63 -22
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/dashboards.py +343 -76
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/main.py +1 -1
- {gaard_api-0.2.12 → gaard_api-0.2.14/src/gaard_api.egg-info}/PKG-INFO +1 -1
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_admin_api.py +284 -11
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_database_migrations.py +15 -5
- {gaard_api-0.2.12 → gaard_api-0.2.14}/MANIFEST.in +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/README.md +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/setup.cfg +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database_legacy_updates.sql +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/defaults.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/migration_runner.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/prompt_runtime.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/identity.js +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/styles.css +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack.min.css +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/package.json +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/analysis.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/prompts.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/query.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/schema.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api_registry.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/auth_dependencies.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/auth_hooks.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/cli_commands.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/conversations.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/settings.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_database.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/extension_services.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/extensions.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/license.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/package_updates.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/py.typed +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/query_hooks.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/server_cli.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/siem.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/tls_http.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/SOURCES.txt +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/requires.txt +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_analysis_api.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_api_cli.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_api_registry.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_conversation_api.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_error_handlers.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_license_api.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_license_service.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_prompt_runtime.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_query_error_messages.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_server_cli.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_settings.py +0 -0
- {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_tls_http.py +0 -0
|
@@ -16,6 +16,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_conversation_turns_turn_id ON conversation_
|
|
|
16
16
|
CREATE TABLE IF NOT EXISTS conversations (id INTEGER NOT NULL, conversation_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, status VARCHAR(50) NOT NULL, datasource_id VARCHAR(255) NOT NULL, datasource_ids_json TEXT NOT NULL, title VARCHAR(255) NOT NULL, summary_json TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id));
|
|
17
17
|
CREATE UNIQUE INDEX IF NOT EXISTS ix_conversations_conversation_id ON conversations (conversation_id);
|
|
18
18
|
CREATE TABLE IF NOT EXISTS dashboard_user_states (owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, active_dashboard_id VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (owner_user_id));
|
|
19
|
+
CREATE TABLE IF NOT EXISTS dashboard_shares (id INTEGER NOT NULL, dashboard_id VARCHAR(64) NOT NULL, target_user_id VARCHAR(255) NOT NULL, target_username VARCHAR(255) NOT NULL, access_level VARCHAR(20) NOT NULL, created_by_user_id VARCHAR(255) NOT NULL, created_by_username VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_shares_dashboard_target UNIQUE (dashboard_id, target_user_id));
|
|
19
20
|
CREATE TABLE IF NOT EXISTS dashboard_widgets (id INTEGER NOT NULL, widget_id VARCHAR(64) NOT NULL, dashboard_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, metric_widget_key VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, visualization_type VARCHAR(50) NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, w INTEGER NOT NULL, h INTEGER NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_widgets_widget_id UNIQUE (widget_id));
|
|
20
21
|
CREATE TABLE IF NOT EXISTS dashboards (id INTEGER NOT NULL, dashboard_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id));
|
|
21
22
|
CREATE UNIQUE INDEX IF NOT EXISTS ix_dashboards_dashboard_id ON dashboards (dashboard_id);
|
|
@@ -63,6 +64,12 @@ CREATE INDEX IF NOT EXISTS ix_conversations_owner_username ON conversations (own
|
|
|
63
64
|
CREATE INDEX IF NOT EXISTS ix_conversations_status ON conversations (status);
|
|
64
65
|
CREATE INDEX IF NOT EXISTS ix_dashboard_user_states_active_dashboard_id ON dashboard_user_states (active_dashboard_id);
|
|
65
66
|
CREATE INDEX IF NOT EXISTS ix_dashboard_user_states_owner_username ON dashboard_user_states (owner_username);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_access_level ON dashboard_shares (access_level);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_user_id ON dashboard_shares (created_by_user_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_username ON dashboard_shares (created_by_username);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_dashboard_id ON dashboard_shares (dashboard_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_user_id ON dashboard_shares (target_user_id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_username ON dashboard_shares (target_username);
|
|
66
73
|
CREATE INDEX IF NOT EXISTS ix_dashboard_widgets_dashboard_id ON dashboard_widgets (dashboard_id);
|
|
67
74
|
CREATE INDEX IF NOT EXISTS ix_dashboard_widgets_metric_widget_key ON dashboard_widgets (metric_widget_key);
|
|
68
75
|
CREATE INDEX IF NOT EXISTS ix_dashboard_widgets_owner_user_id ON dashboard_widgets (owner_user_id);
|
|
@@ -97,6 +104,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_conversation_turns_turn_id ON conversation_
|
|
|
97
104
|
CREATE TABLE IF NOT EXISTS conversations (id SERIAL NOT NULL, conversation_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, status VARCHAR(50) NOT NULL, datasource_id VARCHAR(255) NOT NULL, datasource_ids_json TEXT NOT NULL, title VARCHAR(255) NOT NULL, summary_json TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id));
|
|
98
105
|
CREATE UNIQUE INDEX IF NOT EXISTS ix_conversations_conversation_id ON conversations (conversation_id);
|
|
99
106
|
CREATE TABLE IF NOT EXISTS dashboard_user_states (owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, active_dashboard_id VARCHAR(64) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (owner_user_id));
|
|
107
|
+
CREATE TABLE IF NOT EXISTS dashboard_shares (id SERIAL NOT NULL, dashboard_id VARCHAR(64) NOT NULL, target_user_id VARCHAR(255) NOT NULL, target_username VARCHAR(255) NOT NULL, access_level VARCHAR(20) NOT NULL, created_by_user_id VARCHAR(255) NOT NULL, created_by_username VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_shares_dashboard_target UNIQUE (dashboard_id, target_user_id));
|
|
100
108
|
CREATE TABLE IF NOT EXISTS dashboard_widgets (id SERIAL NOT NULL, widget_id VARCHAR(64) NOT NULL, dashboard_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, metric_widget_key VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, visualization_type VARCHAR(50) NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, w INTEGER NOT NULL, h INTEGER NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_widgets_widget_id UNIQUE (widget_id));
|
|
101
109
|
CREATE TABLE IF NOT EXISTS dashboards (id SERIAL NOT NULL, dashboard_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, owner_username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id));
|
|
102
110
|
CREATE UNIQUE INDEX IF NOT EXISTS ix_dashboards_dashboard_id ON dashboards (dashboard_id);
|
|
@@ -144,6 +152,12 @@ CREATE INDEX IF NOT EXISTS ix_conversations_owner_username ON conversations (own
|
|
|
144
152
|
CREATE INDEX IF NOT EXISTS ix_conversations_status ON conversations (status);
|
|
145
153
|
CREATE INDEX IF NOT EXISTS ix_dashboard_user_states_active_dashboard_id ON dashboard_user_states (active_dashboard_id);
|
|
146
154
|
CREATE INDEX IF NOT EXISTS ix_dashboard_user_states_owner_username ON dashboard_user_states (owner_username);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_access_level ON dashboard_shares (access_level);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_user_id ON dashboard_shares (created_by_user_id);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_username ON dashboard_shares (created_by_username);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_dashboard_id ON dashboard_shares (dashboard_id);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_user_id ON dashboard_shares (target_user_id);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_username ON dashboard_shares (target_username);
|
|
147
161
|
CREATE INDEX IF NOT EXISTS ix_dashboard_widgets_dashboard_id ON dashboard_widgets (dashboard_id);
|
|
148
162
|
CREATE INDEX IF NOT EXISTS ix_dashboard_widgets_metric_widget_key ON dashboard_widgets (metric_widget_key);
|
|
149
163
|
CREATE INDEX IF NOT EXISTS ix_dashboard_widgets_owner_user_id ON dashboard_widgets (owner_user_id);
|
|
@@ -8,3 +8,16 @@ UPDATE identity_privilege_datasource_permissions AS permission SET identity_id =
|
|
|
8
8
|
-- table: identity_privilege_table_permissions
|
|
9
9
|
DELETE FROM identity_privilege_table_permissions AS legacy WHERE EXISTS (SELECT 1 FROM admin_users AS mapped_user WHERE legacy.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username) AND EXISTS (SELECT 1 FROM identity_privilege_table_permissions AS duplicate WHERE duplicate.connector_id = legacy.connector_id AND duplicate.table_name = legacy.table_name AND duplicate.id <> legacy.id AND (duplicate.identity_id = CAST(mapped_user.id AS VARCHAR(255)) OR (duplicate.id < legacy.id AND duplicate.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username)))));
|
|
10
10
|
UPDATE identity_privilege_table_permissions AS permission SET identity_id = (SELECT CAST(mapped_user.id AS VARCHAR(255)) FROM admin_users AS mapped_user WHERE permission.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username) ORDER BY mapped_user.id LIMIT 1) WHERE EXISTS (SELECT 1 FROM admin_users AS mapped_user WHERE permission.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username));
|
|
11
|
+
|
|
12
|
+
-- tag: 2026-08-01.dashboard-sharing.v1
|
|
13
|
+
-- dialect: sqlite
|
|
14
|
+
CREATE TABLE IF NOT EXISTS dashboard_shares (id INTEGER NOT NULL, dashboard_id VARCHAR(64) NOT NULL, target_user_id VARCHAR(255) NOT NULL, target_username VARCHAR(255) NOT NULL, access_level VARCHAR(20) NOT NULL, created_by_user_id VARCHAR(255) NOT NULL, created_by_username VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_shares_dashboard_target UNIQUE (dashboard_id, target_user_id));
|
|
15
|
+
-- dialect: postgresql
|
|
16
|
+
CREATE TABLE IF NOT EXISTS dashboard_shares (id SERIAL NOT NULL, dashboard_id VARCHAR(64) NOT NULL, target_user_id VARCHAR(255) NOT NULL, target_username VARCHAR(255) NOT NULL, access_level VARCHAR(20) NOT NULL, created_by_user_id VARCHAR(255) NOT NULL, created_by_username VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_shares_dashboard_target UNIQUE (dashboard_id, target_user_id));
|
|
17
|
+
-- dialect: all
|
|
18
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_access_level ON dashboard_shares (access_level);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_user_id ON dashboard_shares (created_by_user_id);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_username ON dashboard_shares (created_by_username);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_dashboard_id ON dashboard_shares (dashboard_id);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_user_id ON dashboard_shares (target_user_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_username ON dashboard_shares (target_username);
|
|
@@ -209,6 +209,31 @@ class DashboardUserState(Base):
|
|
|
209
209
|
)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
class DashboardShare(Base):
|
|
213
|
+
__tablename__ = "dashboard_shares"
|
|
214
|
+
__table_args__ = (
|
|
215
|
+
UniqueConstraint(
|
|
216
|
+
"dashboard_id",
|
|
217
|
+
"target_user_id",
|
|
218
|
+
name="uq_dashboard_shares_dashboard_target",
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
223
|
+
dashboard_id: Mapped[str] = mapped_column(String(64), index=True)
|
|
224
|
+
target_user_id: Mapped[str] = mapped_column(String(255), index=True)
|
|
225
|
+
target_username: Mapped[str] = mapped_column(String(255), index=True, default="")
|
|
226
|
+
access_level: Mapped[str] = mapped_column(String(20), index=True)
|
|
227
|
+
created_by_user_id: Mapped[str] = mapped_column(String(255), index=True)
|
|
228
|
+
created_by_username: Mapped[str] = mapped_column(String(255), index=True, default="")
|
|
229
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
|
230
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
231
|
+
DateTime(timezone=True),
|
|
232
|
+
default=utc_now,
|
|
233
|
+
onupdate=utc_now,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
212
237
|
class UserDatasourceSelection(Base):
|
|
213
238
|
"""Client datasource selections, scoped to one authenticated user."""
|
|
214
239
|
|
|
@@ -2229,6 +2229,7 @@ def build_table_settings(
|
|
|
2229
2229
|
tables[table.name] = {
|
|
2230
2230
|
"selected": bool(existing.get("selected", True)),
|
|
2231
2231
|
"description": str(existing.get("description", "")),
|
|
2232
|
+
"view_description": str(existing.get("view_description", "")),
|
|
2232
2233
|
"primary_key_prompt": str(existing.get("primary_key_prompt", "")),
|
|
2233
2234
|
"foreign_key_prompt": str(existing.get("foreign_key_prompt", "")),
|
|
2234
2235
|
"join_logic": str(existing.get("join_logic", "")),
|
|
@@ -100,7 +100,7 @@ var state = {
|
|
|
100
100
|
loadedName: "",
|
|
101
101
|
loadedSql: "",
|
|
102
102
|
name: "",
|
|
103
|
-
|
|
103
|
+
view_description: "",
|
|
104
104
|
sql: "",
|
|
105
105
|
error: "",
|
|
106
106
|
success: "",
|
|
@@ -1926,7 +1926,7 @@ function renderDatasourceViewDialog() {
|
|
|
1926
1926
|
</div>
|
|
1927
1927
|
<form id="datasource-view-form" class="form-grid">
|
|
1928
1928
|
<label>View name<input name="name" value="${escapeHtml(dialog.name || "")}" autocomplete="off" /></label>
|
|
1929
|
-
<label>View description<textarea name="
|
|
1929
|
+
<label>View description<textarea name="view_description">${escapeHtml(dialog.view_description || "")}</textarea></label>
|
|
1930
1930
|
<label>SQL<textarea name="sql" class="textarea-small" spellcheck="false">${escapeHtml(dialog.sql || "")}</textarea></label>
|
|
1931
1931
|
${busy === "load" ? `<p class="muted"><span class="spinner" aria-hidden="true"></span> Loading view definition.</p>` : ""}
|
|
1932
1932
|
${dialog.error ? `<p class="error">${escapeHtml(dialog.error)}</p>` : ""}
|
|
@@ -1976,6 +1976,7 @@ function getDatasourceSchemaDraftTables(rawTables, tableSettings) {
|
|
|
1976
1976
|
state.datasourceSchemaDraftTables[table.name] = {
|
|
1977
1977
|
selected: settings.selected !== false,
|
|
1978
1978
|
description: settings.description || "",
|
|
1979
|
+
view_description: settings.view_description || "",
|
|
1979
1980
|
primary_key_prompt: settings.primary_key_prompt || "",
|
|
1980
1981
|
foreign_key_prompt: settings.foreign_key_prompt || "",
|
|
1981
1982
|
join_logic: settings.join_logic || ""
|
|
@@ -3340,7 +3341,7 @@ function defaultDatasourceViewDialog() {
|
|
|
3340
3341
|
loadedName: "",
|
|
3341
3342
|
loadedSql: "",
|
|
3342
3343
|
name: "",
|
|
3343
|
-
|
|
3344
|
+
view_description: "",
|
|
3344
3345
|
sql: "",
|
|
3345
3346
|
error: "",
|
|
3346
3347
|
success: "",
|
|
@@ -3367,8 +3368,7 @@ async function openEditDatasourceViewDialog(event) {
|
|
|
3367
3368
|
const rawTables = state.datasourceSchema?.item?.raw_schema?.tables || [];
|
|
3368
3369
|
const tableSettings = state.datasourceSchema?.item?.table_settings?.tables || {};
|
|
3369
3370
|
const draftSettings = getDatasourceSchemaDraftTables(rawTables, tableSettings)[viewName] || {};
|
|
3370
|
-
const
|
|
3371
|
-
const draftDescription = hasDraftDescription ? draftSettings.description || "" : "";
|
|
3371
|
+
const draftViewDescription = draftSettings.view_description || "";
|
|
3372
3372
|
state.datasourceViewDialog = {
|
|
3373
3373
|
...defaultDatasourceViewDialog(),
|
|
3374
3374
|
open: true,
|
|
@@ -3376,14 +3376,14 @@ async function openEditDatasourceViewDialog(event) {
|
|
|
3376
3376
|
originalName: viewName,
|
|
3377
3377
|
loadedName: viewName,
|
|
3378
3378
|
name: viewName,
|
|
3379
|
-
|
|
3379
|
+
view_description: draftViewDescription,
|
|
3380
3380
|
busy: "load"
|
|
3381
3381
|
};
|
|
3382
3382
|
render();
|
|
3383
3383
|
try {
|
|
3384
3384
|
const result = await api(`/api/v1/admin/datasources/${selected.id}/schema/views/${encodeURIComponent(viewName)}`);
|
|
3385
3385
|
const item = result.item || {};
|
|
3386
|
-
const
|
|
3386
|
+
const viewDescription = draftViewDescription || item.view_description || item.description || "";
|
|
3387
3387
|
state.datasourceViewDialog = {
|
|
3388
3388
|
...state.datasourceViewDialog,
|
|
3389
3389
|
mode: "edit",
|
|
@@ -3391,7 +3391,7 @@ async function openEditDatasourceViewDialog(event) {
|
|
|
3391
3391
|
loadedName: item.name || viewName,
|
|
3392
3392
|
loadedSql: item.sql || "",
|
|
3393
3393
|
name: item.name || viewName,
|
|
3394
|
-
|
|
3394
|
+
view_description: viewDescription,
|
|
3395
3395
|
sql: item.sql || "",
|
|
3396
3396
|
busy: "",
|
|
3397
3397
|
error: "",
|
|
@@ -3420,14 +3420,14 @@ function readDatasourceViewDialogPayload() {
|
|
|
3420
3420
|
const dialog = state.datasourceViewDialog || {};
|
|
3421
3421
|
return {
|
|
3422
3422
|
name: String(dialog.name || "").trim(),
|
|
3423
|
-
|
|
3423
|
+
view_description: String(dialog.view_description || ""),
|
|
3424
3424
|
sql: String(dialog.sql || "")
|
|
3425
3425
|
};
|
|
3426
3426
|
}
|
|
3427
3427
|
const data = new FormData(form);
|
|
3428
3428
|
return {
|
|
3429
3429
|
name: String(data.get("name") || "").trim(),
|
|
3430
|
-
|
|
3430
|
+
view_description: String(data.get("view_description") || ""),
|
|
3431
3431
|
sql: String(data.get("sql") || "")
|
|
3432
3432
|
};
|
|
3433
3433
|
}
|
|
@@ -3464,7 +3464,7 @@ async function generateDatasourceViewSql() {
|
|
|
3464
3464
|
try {
|
|
3465
3465
|
const result = await api(`/api/v1/admin/datasources/${selected.id}/schema/views/generate-sql`, {
|
|
3466
3466
|
method: "POST",
|
|
3467
|
-
body: JSON.stringify({ name: payload.name,
|
|
3467
|
+
body: JSON.stringify({ name: payload.name, view_description: payload.view_description })
|
|
3468
3468
|
});
|
|
3469
3469
|
state.datasourceViewDialog = {
|
|
3470
3470
|
...state.datasourceViewDialog,
|
|
@@ -3546,7 +3546,7 @@ async function saveDatasourceView(event) {
|
|
|
3546
3546
|
const result = canSaveSettingsOnly
|
|
3547
3547
|
? await api(`/api/v1/admin/datasources/${selected.id}/schema/views/${encodeURIComponent(dialog.originalName || payload.name)}`, {
|
|
3548
3548
|
method: "PUT",
|
|
3549
|
-
body: JSON.stringify({
|
|
3549
|
+
body: JSON.stringify({ view_description: payload.view_description })
|
|
3550
3550
|
})
|
|
3551
3551
|
: dialog.mode === "edit"
|
|
3552
3552
|
? await api(`/api/v1/admin/datasources/${selected.id}/schema/views/${encodeURIComponent(dialog.originalName || payload.name)}/execute`, {
|
|
@@ -3689,6 +3689,7 @@ async function saveDatasourceSchema(event) {
|
|
|
3689
3689
|
tables[table.name] = {
|
|
3690
3690
|
selected: draft.selected !== false,
|
|
3691
3691
|
description: draft.description || "",
|
|
3692
|
+
view_description: draft.view_description || "",
|
|
3692
3693
|
primary_key_prompt: draft.primary_key_prompt || "",
|
|
3693
3694
|
foreign_key_prompt: draft.foreign_key_prompt || "",
|
|
3694
3695
|
join_logic: draft.join_logic || ""
|
|
@@ -363,22 +363,33 @@ class DatasourceSchemaTableSettingsRequest(BaseModel):
|
|
|
363
363
|
class DatasourceViewSqlGenerationRequest(BaseModel):
|
|
364
364
|
name: str = Field(min_length=1, max_length=128)
|
|
365
365
|
description: str = Field(default="", max_length=20000)
|
|
366
|
+
view_description: str | None = Field(default=None, max_length=20000)
|
|
366
367
|
|
|
367
368
|
|
|
368
369
|
class DatasourceViewExecuteRequest(BaseModel):
|
|
369
370
|
name: str = Field(min_length=1, max_length=128)
|
|
370
371
|
description: str = Field(default="", max_length=20000)
|
|
372
|
+
view_description: str | None = Field(default=None, max_length=20000)
|
|
371
373
|
sql: str = Field(min_length=1, max_length=200000)
|
|
372
374
|
|
|
373
375
|
|
|
374
376
|
class DatasourceViewUpdateRequest(BaseModel):
|
|
375
377
|
name: str = Field(min_length=1, max_length=128)
|
|
376
378
|
description: str = Field(default="", max_length=20000)
|
|
379
|
+
view_description: str | None = Field(default=None, max_length=20000)
|
|
377
380
|
sql: str = Field(min_length=1, max_length=200000)
|
|
378
381
|
|
|
379
382
|
|
|
380
383
|
class DatasourceViewSettingsRequest(BaseModel):
|
|
381
384
|
description: str = Field(default="", max_length=20000)
|
|
385
|
+
view_description: str | None = Field(default=None, max_length=20000)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def request_view_description(request: Any) -> str:
|
|
389
|
+
view_description = getattr(request, "view_description", None)
|
|
390
|
+
if view_description is not None:
|
|
391
|
+
return view_description
|
|
392
|
+
return str(getattr(request, "description", ""))
|
|
382
393
|
|
|
383
394
|
|
|
384
395
|
class BusinessLogicSuggestionUpdateRequest(BaseModel):
|
|
@@ -3248,6 +3259,27 @@ def datasource_view_description_from_cache(
|
|
|
3248
3259
|
connector: DatasourceConnector,
|
|
3249
3260
|
view_name: str,
|
|
3250
3261
|
actor: str,
|
|
3262
|
+
) -> str:
|
|
3263
|
+
cache = get_datasource_schema_cache(session, connector.id)
|
|
3264
|
+
if cache is None:
|
|
3265
|
+
cache = introspect_datasource_connector(session, connector, actor)
|
|
3266
|
+
settings = json_loads(cache.table_settings_json)
|
|
3267
|
+
tables = settings.get("tables", {})
|
|
3268
|
+
direct = tables.get(view_name, {})
|
|
3269
|
+
if direct:
|
|
3270
|
+
return str(direct.get("view_description", direct.get("description", "")))
|
|
3271
|
+
lowered = view_name.lower()
|
|
3272
|
+
for name, item in tables.items():
|
|
3273
|
+
if str(name).lower() == lowered:
|
|
3274
|
+
return str(item.get("view_description", item.get("description", "")))
|
|
3275
|
+
return ""
|
|
3276
|
+
|
|
3277
|
+
|
|
3278
|
+
def datasource_view_logic_from_cache(
|
|
3279
|
+
session: Session,
|
|
3280
|
+
connector: DatasourceConnector,
|
|
3281
|
+
view_name: str,
|
|
3282
|
+
actor: str,
|
|
3251
3283
|
) -> str:
|
|
3252
3284
|
cache = get_datasource_schema_cache(session, connector.id)
|
|
3253
3285
|
if cache is None:
|
|
@@ -3282,14 +3314,23 @@ def get_datasource_view_definition(
|
|
|
3282
3314
|
finally:
|
|
3283
3315
|
engine.dispose()
|
|
3284
3316
|
|
|
3317
|
+
view_description = datasource_view_description_from_cache(
|
|
3318
|
+
session,
|
|
3319
|
+
connector,
|
|
3320
|
+
existing_name,
|
|
3321
|
+
actor,
|
|
3322
|
+
)
|
|
3323
|
+
datasource_logic = datasource_view_logic_from_cache(
|
|
3324
|
+
session,
|
|
3325
|
+
connector,
|
|
3326
|
+
existing_name,
|
|
3327
|
+
actor,
|
|
3328
|
+
)
|
|
3285
3329
|
return {
|
|
3286
3330
|
"name": existing_name,
|
|
3287
|
-
"description":
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
existing_name,
|
|
3291
|
-
actor,
|
|
3292
|
-
),
|
|
3331
|
+
"description": view_description,
|
|
3332
|
+
"view_description": view_description,
|
|
3333
|
+
"datasource_logic": datasource_logic,
|
|
3293
3334
|
"sql": view_sql_for_editor(connector, existing_name, definition),
|
|
3294
3335
|
}
|
|
3295
3336
|
|
|
@@ -3298,7 +3339,7 @@ def save_datasource_view_settings_to_cache(
|
|
|
3298
3339
|
session: Session,
|
|
3299
3340
|
cache: DatasourceSchemaCache,
|
|
3300
3341
|
view_name: str,
|
|
3301
|
-
|
|
3342
|
+
view_description: str,
|
|
3302
3343
|
actor: str,
|
|
3303
3344
|
) -> DatasourceSchemaCache:
|
|
3304
3345
|
schema = json_loads(cache.schema_json)
|
|
@@ -3315,7 +3356,7 @@ def save_datasource_view_settings_to_cache(
|
|
|
3315
3356
|
table_settings = settings.setdefault("tables", {})
|
|
3316
3357
|
existing = table_settings.setdefault(actual_view_name, {})
|
|
3317
3358
|
existing["selected"] = True
|
|
3318
|
-
existing["
|
|
3359
|
+
existing["view_description"] = view_description.strip()
|
|
3319
3360
|
return update_schema_table_settings(
|
|
3320
3361
|
session=session,
|
|
3321
3362
|
cache=cache,
|
|
@@ -3328,7 +3369,7 @@ def execute_datasource_view_sql(
|
|
|
3328
3369
|
session: Session,
|
|
3329
3370
|
connector: DatasourceConnector,
|
|
3330
3371
|
view_name: str,
|
|
3331
|
-
|
|
3372
|
+
view_description: str,
|
|
3332
3373
|
sql: str,
|
|
3333
3374
|
actor: str,
|
|
3334
3375
|
) -> DatasourceSchemaCache:
|
|
@@ -3352,7 +3393,7 @@ def execute_datasource_view_sql(
|
|
|
3352
3393
|
session,
|
|
3353
3394
|
cache,
|
|
3354
3395
|
normalized_name,
|
|
3355
|
-
|
|
3396
|
+
view_description,
|
|
3356
3397
|
actor,
|
|
3357
3398
|
)
|
|
3358
3399
|
|
|
@@ -3361,7 +3402,7 @@ def save_datasource_view_settings(
|
|
|
3361
3402
|
session: Session,
|
|
3362
3403
|
connector: DatasourceConnector,
|
|
3363
3404
|
view_name: str,
|
|
3364
|
-
|
|
3405
|
+
view_description: str,
|
|
3365
3406
|
actor: str,
|
|
3366
3407
|
) -> DatasourceSchemaCache:
|
|
3367
3408
|
normalized_name = normalize_datasource_view_name(view_name)
|
|
@@ -3372,7 +3413,7 @@ def save_datasource_view_settings(
|
|
|
3372
3413
|
session,
|
|
3373
3414
|
cache,
|
|
3374
3415
|
normalized_name,
|
|
3375
|
-
|
|
3416
|
+
view_description,
|
|
3376
3417
|
actor,
|
|
3377
3418
|
)
|
|
3378
3419
|
|
|
@@ -3382,7 +3423,7 @@ def update_datasource_view_sql(
|
|
|
3382
3423
|
connector: DatasourceConnector,
|
|
3383
3424
|
current_view_name: str,
|
|
3384
3425
|
next_view_name: str,
|
|
3385
|
-
|
|
3426
|
+
view_description: str,
|
|
3386
3427
|
sql: str,
|
|
3387
3428
|
actor: str,
|
|
3388
3429
|
) -> DatasourceSchemaCache:
|
|
@@ -3416,7 +3457,7 @@ def update_datasource_view_sql(
|
|
|
3416
3457
|
session,
|
|
3417
3458
|
cache,
|
|
3418
3459
|
next_name,
|
|
3419
|
-
|
|
3460
|
+
view_description,
|
|
3420
3461
|
actor,
|
|
3421
3462
|
)
|
|
3422
3463
|
|
|
@@ -3448,11 +3489,11 @@ def generate_datasource_view_sql(
|
|
|
3448
3489
|
session: Session,
|
|
3449
3490
|
connector: DatasourceConnector,
|
|
3450
3491
|
view_name: str,
|
|
3451
|
-
|
|
3492
|
+
view_description: str,
|
|
3452
3493
|
actor: str,
|
|
3453
3494
|
) -> str:
|
|
3454
3495
|
normalized_name = normalize_datasource_view_name(view_name)
|
|
3455
|
-
if not
|
|
3496
|
+
if not view_description.strip():
|
|
3456
3497
|
raise ValueError("View description is required to generate SQL.")
|
|
3457
3498
|
|
|
3458
3499
|
cache = get_or_create_datasource_schema_cache(session, connector, actor)
|
|
@@ -3464,8 +3505,8 @@ def generate_datasource_view_sql(
|
|
|
3464
3505
|
query_request = QueryRequest(
|
|
3465
3506
|
question=(
|
|
3466
3507
|
f"Create a SQL SELECT statement for a database view named {normalized_name}. "
|
|
3467
|
-
"The SELECT must implement this
|
|
3468
|
-
f"{
|
|
3508
|
+
"The SELECT must implement this view description:\n"
|
|
3509
|
+
f"{view_description.strip()}\n\n"
|
|
3469
3510
|
"Return only one SELECT query. Do not create, drop, alter, insert, update or delete data."
|
|
3470
3511
|
),
|
|
3471
3512
|
datasource_id=connector.connector_key,
|
|
@@ -3615,7 +3656,7 @@ def generate_datasource_view_sql_preview(
|
|
|
3615
3656
|
session=session,
|
|
3616
3657
|
connector=connector,
|
|
3617
3658
|
view_name=request.name,
|
|
3618
|
-
|
|
3659
|
+
view_description=request_view_description(request),
|
|
3619
3660
|
actor=user.username,
|
|
3620
3661
|
)
|
|
3621
3662
|
except (
|
|
@@ -3656,7 +3697,7 @@ def execute_datasource_view_sql_endpoint(
|
|
|
3656
3697
|
session=session,
|
|
3657
3698
|
connector=connector,
|
|
3658
3699
|
view_name=request.name,
|
|
3659
|
-
|
|
3700
|
+
view_description=request_view_description(request),
|
|
3660
3701
|
sql=request.sql,
|
|
3661
3702
|
actor=user.username,
|
|
3662
3703
|
)
|
|
@@ -3705,7 +3746,7 @@ def update_datasource_view_sql_endpoint(
|
|
|
3705
3746
|
connector=connector,
|
|
3706
3747
|
current_view_name=view_name,
|
|
3707
3748
|
next_view_name=request.name,
|
|
3708
|
-
|
|
3749
|
+
view_description=request_view_description(request),
|
|
3709
3750
|
sql=request.sql,
|
|
3710
3751
|
actor=user.username,
|
|
3711
3752
|
)
|
|
@@ -3756,7 +3797,7 @@ def save_datasource_view_settings_endpoint(
|
|
|
3756
3797
|
session=session,
|
|
3757
3798
|
connector=connector,
|
|
3758
3799
|
view_name=view_name,
|
|
3759
|
-
|
|
3800
|
+
view_description=request_view_description(request),
|
|
3760
3801
|
actor=user.username,
|
|
3761
3802
|
)
|
|
3762
3803
|
except (SQLAlchemyError, ValueError) as exc:
|