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.
Files changed (78) hide show
  1. {gaard_api-0.2.12/src/gaard_api.egg-info → gaard_api-0.2.14}/PKG-INFO +1 -1
  2. {gaard_api-0.2.12 → gaard_api-0.2.14}/pyproject.toml +1 -1
  3. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database_initial.sql +14 -0
  4. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database_updates.sql +13 -0
  5. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/models.py +25 -0
  6. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/services.py +1 -0
  7. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/main.js +13 -12
  8. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/admin.py +63 -22
  9. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/dashboards.py +343 -76
  10. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/main.py +1 -1
  11. {gaard_api-0.2.12 → gaard_api-0.2.14/src/gaard_api.egg-info}/PKG-INFO +1 -1
  12. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_admin_api.py +284 -11
  13. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_database_migrations.py +15 -5
  14. {gaard_api-0.2.12 → gaard_api-0.2.14}/MANIFEST.in +0 -0
  15. {gaard_api-0.2.12 → gaard_api-0.2.14}/README.md +0 -0
  16. {gaard_api-0.2.12 → gaard_api-0.2.14}/setup.cfg +0 -0
  17. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/__init__.py +0 -0
  18. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/__init__.py +0 -0
  19. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database.py +0 -0
  20. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/database_legacy_updates.sql +0 -0
  21. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/defaults.py +0 -0
  22. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/migration_runner.py +0 -0
  23. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/prompt_runtime.py +0 -0
  24. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin/security.py +0 -0
  25. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
  26. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/identity.js +0 -0
  27. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/styles.css +0 -0
  28. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js +0 -0
  29. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack.min.css +0 -0
  30. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/index.html +0 -0
  31. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/admin-web/package.json +0 -0
  32. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/__init__.py +0 -0
  33. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/__init__.py +0 -0
  34. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/analysis.py +0 -0
  35. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/prompts.py +0 -0
  36. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/query.py +0 -0
  37. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api/v1/schema.py +0 -0
  38. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/api_registry.py +0 -0
  39. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/auth_dependencies.py +0 -0
  40. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/auth_hooks.py +0 -0
  41. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/cli.py +0 -0
  42. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/cli_commands.py +0 -0
  43. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/conversations.py +0 -0
  44. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/__init__.py +0 -0
  45. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/error_handlers.py +0 -0
  46. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/schema_cache.py +0 -0
  47. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/core/settings.py +0 -0
  48. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/__init__.py +0 -0
  49. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
  50. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
  51. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
  52. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/example_database.py +0 -0
  53. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/extension_services.py +0 -0
  54. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/extensions.py +0 -0
  55. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/license.py +0 -0
  56. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/package_updates.py +0 -0
  57. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/py.typed +0 -0
  58. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/query_hooks.py +0 -0
  59. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/server_cli.py +0 -0
  60. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/siem.py +0 -0
  61. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api/tls_http.py +0 -0
  62. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/SOURCES.txt +0 -0
  63. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/dependency_links.txt +0 -0
  64. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/entry_points.txt +0 -0
  65. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/requires.txt +0 -0
  66. {gaard_api-0.2.12 → gaard_api-0.2.14}/src/gaard_api.egg-info/top_level.txt +0 -0
  67. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_analysis_api.py +0 -0
  68. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_api_cli.py +0 -0
  69. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_api_registry.py +0 -0
  70. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_conversation_api.py +0 -0
  71. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_error_handlers.py +0 -0
  72. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_license_api.py +0 -0
  73. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_license_service.py +0 -0
  74. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_prompt_runtime.py +0 -0
  75. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_query_error_messages.py +0 -0
  76. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_server_cli.py +0 -0
  77. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_settings.py +0 -0
  78. {gaard_api-0.2.12 → gaard_api-0.2.14}/tests/test_tls_http.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-api
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: GAARD backend web services providing admin interface
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gaard-api"
7
- version = "0.2.12"
7
+ version = "0.2.14"
8
8
  description = "GAARD backend web services providing admin interface"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
- description: "",
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="description">${escapeHtml(dialog.description || "")}</textarea></label>
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
- description: "",
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 hasDraftDescription = Object.prototype.hasOwnProperty.call(draftSettings, "description");
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
- description: draftDescription,
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 description = hasDraftDescription ? draftDescription : item.description || "";
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
- description,
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
- description: String(dialog.description || ""),
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
- description: String(data.get("description") || ""),
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, description: payload.description })
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({ description: payload.description })
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": datasource_view_description_from_cache(
3288
- session,
3289
- connector,
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
- description: str,
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["description"] = description.strip()
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
- description: str,
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
- description,
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
- description: str,
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
- description,
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
- description: str,
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
- description,
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
- description: str,
3492
+ view_description: str,
3452
3493
  actor: str,
3453
3494
  ) -> str:
3454
3495
  normalized_name = normalize_datasource_view_name(view_name)
3455
- if not description.strip():
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 datasource logic:\n"
3468
- f"{description.strip()}\n\n"
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
- description=request.description,
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
- description=request.description,
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
- description=request.description,
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
- description=request.description,
3800
+ view_description=request_view_description(request),
3760
3801
  actor=user.username,
3761
3802
  )
3762
3803
  except (SQLAlchemyError, ValueError) as exc: