From 8724aaa0e20027899b323bccc388e3fe8ca78ef9 Mon Sep 17 00:00:00 2001 From: Oleh Martsokha Date: Tue, 28 Jul 2026 04:31:10 +0200 Subject: [PATCH] Drop contexts resource; bump runtime; drop direct nvisy-schema dep Upstream runtime dropped the nvisy-context crate and the `contexts` parameter from `Engine::analyze_document`, so the server's contexts resource has no backing engine support. Remove it end to end: - DB: drop the workspace_contexts table and the workspace_pipeline_contexts join; delete their model, query, and constraint modules; regenerate schema.rs. - Server: delete the contexts handler, request/response DTOs, routes, the Contexts builtin module, the ViewContexts/ManageContexts permissions, the OpenAPI tag, and the pipeline/run context references (context_slugs, resolve_contexts, the analyze_document contexts argument). Reorder migrations so pipelines (045014) run before policies (045015), and move the workspace_pipeline_policies join into the policies migration where both parent tables exist. The full up/down chain applies and reverts cleanly. Bump the runtime git dep to 4e8f32d4 and drop the direct nvisy-schema dependency: nvisy-engine now re-exports file/plan/policy, so the server consumes those types through nvisy_engine instead. nvisy-schema remains only as a transitive dependency of the engine. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 177 ++------- Cargo.toml | 1 - crates/nvisy-postgres/src/model/mod.rs | 4 +- .../src/model/pipeline_reference.rs | 19 +- .../src/model/workspace_context.rs | 110 ------ crates/nvisy-postgres/src/query/mod.rs | 2 - .../src/query/pipeline_reference.rs | 133 +------ .../src/query/workspace_context.rs | 283 -------------- crates/nvisy-postgres/src/schema.rs | 34 -- .../src/types/constraint/mod.rs | 8 - .../types/constraint/pipeline_references.rs | 10 +- .../types/constraint/workspace_contexts.rs | 92 ----- crates/nvisy-postgres/src/types/mod.rs | 4 +- crates/nvisy-server/Cargo.toml | 1 - .../src/extract/auth/permission.rs | 10 +- crates/nvisy-server/src/handler/contexts.rs | 346 ------------------ .../src/handler/error/pg_error.rs | 1 - .../src/handler/error/pg_pipeline.rs | 44 +-- crates/nvisy-server/src/handler/mod.rs | 4 - crates/nvisy-server/src/handler/pipelines.rs | 47 +-- .../src/handler/request/contexts.rs | 55 --- .../nvisy-server/src/handler/request/mod.rs | 2 - .../src/handler/request/pipeline_runs.rs | 2 +- .../src/handler/request/pipelines.rs | 59 +-- .../src/handler/request/policies.rs | 2 +- .../src/handler/response/contexts.rs | 64 ---- .../nvisy-server/src/handler/response/mod.rs | 2 - .../src/handler/response/pipelines.rs | 14 +- .../src/handler/response/policies.rs | 2 +- crates/nvisy-server/src/handler/runs.rs | 37 +- .../src/handler/utility/custom_routes.rs | 2 - .../src/middleware/specification.rs | 5 - .../down.sql | 4 - .../up.sql | 48 +-- .../2026-01-19-045014_policies/down.sql | 3 - .../2026-01-19-045015_contexts/down.sql | 3 - migrations/2026-01-19-045015_contexts/up.sql | 84 ----- .../2026-01-19-045015_policies/down.sql | 6 + .../up.sql | 22 ++ 39 files changed, 128 insertions(+), 1618 deletions(-) delete mode 100644 crates/nvisy-postgres/src/model/workspace_context.rs delete mode 100644 crates/nvisy-postgres/src/query/workspace_context.rs delete mode 100644 crates/nvisy-postgres/src/types/constraint/workspace_contexts.rs delete mode 100644 crates/nvisy-server/src/handler/contexts.rs delete mode 100644 crates/nvisy-server/src/handler/request/contexts.rs delete mode 100644 crates/nvisy-server/src/handler/response/contexts.rs rename migrations/{2026-01-19-045016_pipelines => 2026-01-19-045014_pipelines}/down.sql (73%) rename migrations/{2026-01-19-045016_pipelines => 2026-01-19-045014_pipelines}/up.sql (85%) delete mode 100644 migrations/2026-01-19-045014_policies/down.sql delete mode 100644 migrations/2026-01-19-045015_contexts/down.sql delete mode 100644 migrations/2026-01-19-045015_contexts/up.sql create mode 100644 migrations/2026-01-19-045015_policies/down.sql rename migrations/{2026-01-19-045014_policies => 2026-01-19-045015_policies}/up.sql (78%) diff --git a/Cargo.lock b/Cargo.lock index 0ae664c..8092e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,7 +849,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -1218,7 +1218,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.117", ] @@ -1232,7 +1232,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.117", ] @@ -1245,7 +1245,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.117", ] @@ -1354,47 +1354,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "deluxe" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" -dependencies = [ - "deluxe-core", - "deluxe-macros", - "once_cell", - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "deluxe-core" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" -dependencies = [ - "arrayvec", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 2.0.117", -] - -[[package]] -name = "deluxe-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" -dependencies = [ - "deluxe-core", - "heck 0.4.1", - "if_chain", - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "der" version = "0.7.10" @@ -1699,7 +1658,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elide" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "elide-codec", @@ -1719,7 +1678,7 @@ dependencies = [ [[package]] name = "elide-bento" version = "0.1.0" -source = "git+https://github.com/nvisycom/runtime?branch=main#1ac5940148789fe405b709e3a22d3fa20792c522" +source = "git+https://github.com/nvisycom/runtime?branch=main#4e8f32d4f75e46e11952c75238b8ebad02834d96" dependencies = [ "async-trait", "base64", @@ -1735,7 +1694,7 @@ dependencies = [ [[package]] name = "elide-codec" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "bytes", @@ -1758,7 +1717,7 @@ dependencies = [ [[package]] name = "elide-context" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "elide-core", @@ -1769,7 +1728,7 @@ dependencies = [ [[package]] name = "elide-core" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "bytes", @@ -1787,7 +1746,7 @@ dependencies = [ [[package]] name = "elide-detection" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "elide-core", @@ -1800,7 +1759,7 @@ dependencies = [ [[package]] name = "elide-lingua" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "elide-core", @@ -1811,7 +1770,7 @@ dependencies = [ [[package]] name = "elide-llm" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "derive_builder", @@ -1831,7 +1790,7 @@ dependencies = [ [[package]] name = "elide-ner" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "derive_builder", @@ -1845,7 +1804,7 @@ dependencies = [ [[package]] name = "elide-ocr" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "elide-core", @@ -1855,7 +1814,7 @@ dependencies = [ [[package]] name = "elide-orchestration" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "bytes", "elide-codec", @@ -1869,7 +1828,7 @@ dependencies = [ [[package]] name = "elide-pattern" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "aho-corasick", "async-trait", @@ -1889,7 +1848,7 @@ dependencies = [ [[package]] name = "elide-redaction" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "bytes", @@ -1904,7 +1863,7 @@ dependencies = [ [[package]] name = "elide-stt" version = "0.1.0" -source = "git+https://github.com/nvisycom/elide?branch=main#33dea52314905c633d9bbd9a30486bca6bfb01ea" +source = "git+https://github.com/nvisycom/elide?branch=main#a29a152ec00d3d2d3ca36ee4b9e55a867744b17e" dependencies = [ "async-trait", "elide-core", @@ -2843,12 +2802,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "if_chain" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" - [[package]] name = "image" version = "0.25.10" @@ -3496,15 +3449,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand 0.8.6", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3712,21 +3656,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "nvisy-context" -version = "0.1.0" -source = "git+https://github.com/nvisycom/runtime?branch=main#1ac5940148789fe405b709e3a22d3fa20792c522" -dependencies = [ - "derive_builder", - "derive_more", - "elide-core", - "jiff", - "schemars", - "semver", - "serde", - "uuid", -] - [[package]] name = "nvisy-core" version = "0.1.0" @@ -3739,7 +3668,7 @@ dependencies = [ [[package]] name = "nvisy-engine" version = "0.1.0" -source = "git+https://github.com/nvisycom/runtime?branch=main#1ac5940148789fe405b709e3a22d3fa20792c522" +source = "git+https://github.com/nvisycom/runtime?branch=main#4e8f32d4f75e46e11952c75238b8ebad02834d96" dependencies = [ "bytes", "derive_builder", @@ -3795,7 +3724,7 @@ dependencies = [ [[package]] name = "nvisy-policy" version = "0.1.0" -source = "git+https://github.com/nvisycom/runtime?branch=main#1ac5940148789fe405b709e3a22d3fa20792c522" +source = "git+https://github.com/nvisycom/runtime?branch=main#4e8f32d4f75e46e11952c75238b8ebad02834d96" dependencies = [ "derive_more", "elide-core", @@ -3840,13 +3769,12 @@ dependencies = [ [[package]] name = "nvisy-schema" version = "0.1.0" -source = "git+https://github.com/nvisycom/runtime?branch=main#1ac5940148789fe405b709e3a22d3fa20792c522" +source = "git+https://github.com/nvisycom/runtime?branch=main#4e8f32d4f75e46e11952c75238b8ebad02834d96" dependencies = [ "bytes", "elide-core", "hipstr", "jiff", - "nvisy-context", "nvisy-policy", "schemars", "serde", @@ -3881,7 +3809,6 @@ dependencies = [ "nvisy-engine", "nvisy-nats", "nvisy-postgres", - "nvisy-schema", "nvisy-webhook", "pin-project-lite", "rand 0.10.2", @@ -4387,23 +4314,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit", ] [[package]] @@ -4932,18 +4849,18 @@ checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" [[package]] name = "rig" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98e2e8f01c4c5bc23f577983634fa4d5244ffb070ea14c23b1ea5bd406e5cac" +checksum = "9aac59326725633a25c21ef2c24d33c4d00dc38a1f0cc097205a7a9bde70b482" dependencies = [ "rig-core", ] [[package]] name = "rig-core" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a4bc7a93b329c4e1a66d5fd211d79990e7331e3c701f057c29f135f548686d" +checksum = "d8731dd5532b3a12ce1613af73073fb2051ef750f50c504778c21d55ae933cac" dependencies = [ "as-any", "async-stream", @@ -4958,7 +4875,6 @@ dependencies = [ "indexmap", "mime", "mime_guess", - "nanoid", "ordered-float", "pin-project-lite", "reqwest", @@ -4977,14 +4893,13 @@ dependencies = [ [[package]] name = "rig-derive" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5531bfa887b371eab658a92de7db35003370bbeee208ff5e68bbb81a5ae92d3d" +checksum = "b3e98dde7a4e59e083e7396126ee4c83498c5bff605d126654e67815fa230a78" dependencies = [ "convert_case 0.11.0", - "deluxe", "indoc", - "proc-macro-crate 3.5.0", + "proc-macro-crate", "proc-macro2", "quote", "serde_json", @@ -5669,12 +5584,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -5852,7 +5761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -6119,12 +6028,6 @@ dependencies = [ "winnow 1.0.3", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -6143,17 +6046,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.25.12+spec-1.1.0" @@ -7099,15 +6991,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index 341872a..ddba352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ documentation = "https://docs.rs/nvisy-server" # Runtime crates nvisy-engine = { git = "https://github.com/nvisycom/runtime", branch = "main" } -nvisy-schema = { git = "https://github.com/nvisycom/runtime", branch = "main", default-features = false } # Internal crates nvisy-core = { path = "./crates/nvisy-core", version = "0.1.0" } diff --git a/crates/nvisy-postgres/src/model/mod.rs b/crates/nvisy-postgres/src/model/mod.rs index 9b186f3..c6b0bab 100644 --- a/crates/nvisy-postgres/src/model/mod.rs +++ b/crates/nvisy-postgres/src/model/mod.rs @@ -11,7 +11,6 @@ mod workspace; mod workspace_activity; mod workspace_connection; mod workspace_connection_run; -mod workspace_context; mod workspace_file; mod workspace_invite; mod workspace_member; @@ -27,7 +26,7 @@ pub use account_api_token::{AccountApiToken, NewAccountApiToken, UpdateAccountAp pub use account_notification::{ AccountNotification, NewAccountNotification, UpdateAccountNotification, }; -pub use pipeline_reference::{PipelineContext, PipelinePolicy}; +pub use pipeline_reference::PipelinePolicy; // Workspace models pub use workspace::{NewWorkspace, UpdateWorkspace, Workspace}; pub use workspace_activity::{NewWorkspaceActivity, WorkspaceActivity}; @@ -37,7 +36,6 @@ pub use workspace_connection::{ pub use workspace_connection_run::{ NewWorkspaceConnectionRun, UpdateWorkspaceConnectionRun, WorkspaceConnectionRun, }; -pub use workspace_context::{NewWorkspaceContext, UpdateWorkspaceContext, WorkspaceContext}; pub use workspace_file::{NewWorkspaceFile, UpdateWorkspaceFile, WorkspaceFile}; pub use workspace_invite::{NewWorkspaceInvite, UpdateWorkspaceInvite, WorkspaceInvite}; pub use workspace_member::{NewWorkspaceMember, UpdateWorkspaceMember, WorkspaceMember}; diff --git a/crates/nvisy-postgres/src/model/pipeline_reference.rs b/crates/nvisy-postgres/src/model/pipeline_reference.rs index 8948e4a..06c1027 100644 --- a/crates/nvisy-postgres/src/model/pipeline_reference.rs +++ b/crates/nvisy-postgres/src/model/pipeline_reference.rs @@ -1,13 +1,13 @@ -//! Join models linking a pipeline to the policies and contexts it references. +//! Join model linking a pipeline to the policies it references. //! //! References are relational (real foreign keys) rather than embedded in the //! pipeline's JSON definition, so the database enforces integrity and cleans up -//! on cascade. The composite keys pin every reference to a single workspace. +//! on cascade. The composite key pins every reference to a single workspace. use diesel::prelude::*; use uuid::Uuid; -use crate::schema::{workspace_pipeline_contexts, workspace_pipeline_policies}; +use crate::schema::workspace_pipeline_policies; /// A pipeline → policy reference row. #[derive(Debug, Clone, PartialEq, Queryable, Selectable, Insertable)] @@ -21,16 +21,3 @@ pub struct PipelinePolicy { /// Referenced policy. pub policy_id: Uuid, } - -/// A pipeline → context reference row. -#[derive(Debug, Clone, PartialEq, Queryable, Selectable, Insertable)] -#[diesel(table_name = workspace_pipeline_contexts)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct PipelineContext { - /// Workspace both the pipeline and context belong to. - pub workspace_id: Uuid, - /// Referencing pipeline. - pub pipeline_id: Uuid, - /// Referenced context. - pub context_id: Uuid, -} diff --git a/crates/nvisy-postgres/src/model/workspace_context.rs b/crates/nvisy-postgres/src/model/workspace_context.rs deleted file mode 100644 index 8fb0f88..0000000 --- a/crates/nvisy-postgres/src/model/workspace_context.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Workspace context model for PostgreSQL database operations. - -use diesel::prelude::*; -use jiff_diesel::Timestamp; -use serde_json::Value as JsonValue; -use uuid::Uuid; - -use crate::schema::workspace_contexts; -use crate::types::{HasCreatedAt, HasDeletedAt, HasUpdatedAt, Slug}; - -/// Workspace context representing structured reference-data for redaction. -/// -/// The `definition` holds a `nvisy_schema` Context (typed reference-data -/// entries) that the redaction engine consumes. -#[derive(Debug, Clone, PartialEq, Queryable, Selectable)] -#[diesel(table_name = workspace_contexts)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct WorkspaceContext { - /// Unique context identifier. - pub id: Uuid, - /// Reference to the workspace this context belongs to. - pub workspace_id: Uuid, - /// Reference to the account that created this context. - pub account_id: Uuid, - /// URL-safe context identifier, unique within the workspace. - pub slug: Slug, - /// Human-readable context name. - pub name: String, - /// Context description. - pub description: Option, - /// Semver of the context body. - pub version: String, - /// Encrypted Context body (the engine's Context type as JSON). - pub definition: Vec, - /// Metadata for filtering/display. - pub metadata: JsonValue, - /// Timestamp when the context was created. - pub created_at: Timestamp, - /// Timestamp when the context was last updated. - pub updated_at: Timestamp, - /// Timestamp when the context was soft-deleted. - pub deleted_at: Option, -} - -/// Data for creating a new workspace context. -#[derive(Debug, Clone, Insertable)] -#[diesel(table_name = workspace_contexts)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct NewWorkspaceContext { - /// Workspace ID (required). - pub workspace_id: Uuid, - /// Account ID (required). - pub account_id: Uuid, - /// URL-safe context identifier, unique within the workspace. - pub slug: Slug, - /// Context name. - pub name: String, - /// Context description. - pub description: Option, - /// Semver of the context body. - pub version: String, - /// Encrypted Context body (the engine's Context type as JSON). - pub definition: Vec, - /// Metadata for filtering/display. - pub metadata: Option, -} - -/// Data for updating a workspace context. -#[derive(Debug, Clone, Default, AsChangeset)] -#[diesel(table_name = workspace_contexts)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct UpdateWorkspaceContext { - /// Context name. - pub name: Option, - /// Context description. - pub description: Option>, - /// Semver of the context body. - pub version: Option, - /// Encrypted Context body (the engine's Context type as JSON). - pub definition: Option>, - /// Metadata for filtering/display. - pub metadata: Option, - /// Soft delete timestamp. - pub deleted_at: Option>, -} - -impl WorkspaceContext { - /// Returns whether the context is deleted. - pub fn is_deleted(&self) -> bool { - self.deleted_at.is_some() - } -} - -impl HasCreatedAt for WorkspaceContext { - fn created_at(&self) -> jiff::Timestamp { - self.created_at.into() - } -} - -impl HasUpdatedAt for WorkspaceContext { - fn updated_at(&self) -> jiff::Timestamp { - self.updated_at.into() - } -} - -impl HasDeletedAt for WorkspaceContext { - fn deleted_at(&self) -> Option { - self.deleted_at.map(Into::into) - } -} diff --git a/crates/nvisy-postgres/src/query/mod.rs b/crates/nvisy-postgres/src/query/mod.rs index bb2efcd..b5aa74b 100644 --- a/crates/nvisy-postgres/src/query/mod.rs +++ b/crates/nvisy-postgres/src/query/mod.rs @@ -21,7 +21,6 @@ mod workspace; mod workspace_activity; mod workspace_connection; mod workspace_connection_run; -mod workspace_context; mod workspace_file; mod workspace_invite; mod workspace_member; @@ -39,7 +38,6 @@ pub use workspace::WorkspaceRepository; pub use workspace_activity::WorkspaceActivityRepository; pub use workspace_connection::WorkspaceConnectionRepository; pub use workspace_connection_run::WorkspaceConnectionRunRepository; -pub use workspace_context::WorkspaceContextRepository; pub use workspace_file::WorkspaceFileRepository; pub use workspace_invite::WorkspaceInviteRepository; pub use workspace_member::WorkspaceMemberRepository; diff --git a/crates/nvisy-postgres/src/query/pipeline_reference.rs b/crates/nvisy-postgres/src/query/pipeline_reference.rs index 2ab26be..8734480 100644 --- a/crates/nvisy-postgres/src/query/pipeline_reference.rs +++ b/crates/nvisy-postgres/src/query/pipeline_reference.rs @@ -1,10 +1,9 @@ -//! Repository for a pipeline's policy and context references. +//! Repository for a pipeline's policy references. //! -//! References live in join tables (`workspace_pipeline_policies`, `workspace_pipeline_contexts`) -//! rather than the pipeline's JSON definition, so foreign keys enforce that -//! every referenced policy/context exists in the pipeline's workspace. The -//! `replace_*` operations are delete-then-insert and expect to run inside a -//! caller-owned transaction. +//! References live in the `workspace_pipeline_policies` join table rather than +//! the pipeline's JSON definition, so foreign keys enforce that every referenced +//! policy exists in the pipeline's workspace. The `replace_*` operation is +//! delete-then-insert and expects to run inside a caller-owned transaction. use std::future::Future; @@ -12,7 +11,7 @@ use diesel::prelude::*; use diesel_async::RunQueryDsl; use uuid::Uuid; -use crate::model::{PipelineContext, PipelinePolicy}; +use crate::model::PipelinePolicy; use crate::types::Slug; use crate::{PgConnection, PgError, PgResult, schema}; @@ -29,14 +28,6 @@ pub trait PipelineReferenceRepository { policy_ids: &[Uuid], ) -> impl Future> + Send; - /// Replaces a pipeline's context references with the given set. - fn replace_workspace_pipeline_contexts( - &mut self, - workspace_id: Uuid, - pipeline_id: Uuid, - context_ids: &[Uuid], - ) -> impl Future> + Send; - /// Lists the ids of the policies a pipeline references. /// /// Used by the run path to resolve each referenced policy to its record for @@ -46,24 +37,12 @@ pub trait PipelineReferenceRepository { pipeline_id: Uuid, ) -> impl Future>> + Send; - /// Lists the ids of the contexts a pipeline references. - fn list_pipeline_context_ids( - &mut self, - pipeline_id: Uuid, - ) -> impl Future>> + Send; - /// Lists the slugs of the policies a pipeline references. fn list_pipeline_policy_slugs( &mut self, pipeline_id: Uuid, ) -> impl Future>> + Send; - /// Lists the slugs of the contexts a pipeline references. - fn list_pipeline_context_slugs( - &mut self, - pipeline_id: Uuid, - ) -> impl Future>> + Send; - /// Resolves policy slugs to their ids within a workspace, preserving order. /// /// Returns `None` if any slug does not match a live policy in the workspace, @@ -74,13 +53,6 @@ pub trait PipelineReferenceRepository { workspace_id: Uuid, slugs: &[Slug], ) -> impl Future>>> + Send; - - /// Resolves context slugs to their ids within a workspace, preserving order. - fn resolve_context_slugs( - &mut self, - workspace_id: Uuid, - slugs: &[Slug], - ) -> impl Future>>> + Send; } impl PipelineReferenceRepository for PgConnection { @@ -117,39 +89,6 @@ impl PipelineReferenceRepository for PgConnection { Ok(()) } - async fn replace_workspace_pipeline_contexts( - &mut self, - workspace_id: Uuid, - pipeline_id: Uuid, - context_ids: &[Uuid], - ) -> PgResult<()> { - use schema::workspace_pipeline_contexts::{self, dsl}; - - diesel::delete(workspace_pipeline_contexts::table.filter(dsl::pipeline_id.eq(pipeline_id))) - .execute(self) - .await - .map_err(PgError::from)?; - - if !context_ids.is_empty() { - let rows: Vec = dedup(context_ids) - .into_iter() - .map(|context_id| PipelineContext { - workspace_id, - pipeline_id, - context_id, - }) - .collect(); - - diesel::insert_into(workspace_pipeline_contexts::table) - .values(&rows) - .execute(self) - .await - .map_err(PgError::from)?; - } - - Ok(()) - } - async fn list_pipeline_policy_ids(&mut self, pipeline_id: Uuid) -> PgResult> { use schema::{workspace_pipeline_policies, workspace_policies}; @@ -168,24 +107,6 @@ impl PipelineReferenceRepository for PgConnection { Ok(ids) } - async fn list_pipeline_context_ids(&mut self, pipeline_id: Uuid) -> PgResult> { - use schema::{workspace_contexts, workspace_pipeline_contexts}; - - let ids = workspace_pipeline_contexts::table - .inner_join( - workspace_contexts::table - .on(workspace_contexts::id.eq(workspace_pipeline_contexts::context_id)), - ) - .filter(workspace_pipeline_contexts::pipeline_id.eq(pipeline_id)) - .filter(workspace_contexts::deleted_at.is_null()) - .select(workspace_pipeline_contexts::context_id) - .load(self) - .await - .map_err(PgError::from)?; - - Ok(ids) - } - async fn list_pipeline_policy_slugs(&mut self, pipeline_id: Uuid) -> PgResult> { use schema::{workspace_pipeline_policies, workspace_policies}; @@ -206,24 +127,6 @@ impl PipelineReferenceRepository for PgConnection { Ok(slugs) } - async fn list_pipeline_context_slugs(&mut self, pipeline_id: Uuid) -> PgResult> { - use schema::{workspace_contexts, workspace_pipeline_contexts}; - - let slugs = workspace_pipeline_contexts::table - .inner_join( - workspace_contexts::table - .on(workspace_contexts::id.eq(workspace_pipeline_contexts::context_id)), - ) - .filter(workspace_pipeline_contexts::pipeline_id.eq(pipeline_id)) - .filter(workspace_contexts::deleted_at.is_null()) - .select(workspace_contexts::slug) - .load(self) - .await - .map_err(PgError::from)?; - - Ok(slugs) - } - async fn resolve_policy_slugs( &mut self, workspace_id: Uuid, @@ -247,30 +150,6 @@ impl PipelineReferenceRepository for PgConnection { Ok(map_slugs_to_ids(slugs, found)) } - - async fn resolve_context_slugs( - &mut self, - workspace_id: Uuid, - slugs: &[Slug], - ) -> PgResult>> { - use schema::workspace_contexts::{self, dsl}; - - if slugs.is_empty() { - return Ok(Some(Vec::new())); - } - - let wanted: Vec = slugs.iter().map(|slug| slug.as_str().to_owned()).collect(); - let found: Vec<(Slug, Uuid)> = workspace_contexts::table - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::deleted_at.is_null()) - .filter(dsl::slug.eq_any(&wanted)) - .select((dsl::slug, dsl::id)) - .load(self) - .await - .map_err(PgError::from)?; - - Ok(map_slugs_to_ids(slugs, found)) - } } /// Maps the requested slugs to ids in request order, returning `None` if any diff --git a/crates/nvisy-postgres/src/query/workspace_context.rs b/crates/nvisy-postgres/src/query/workspace_context.rs deleted file mode 100644 index 8dbc00b..0000000 --- a/crates/nvisy-postgres/src/query/workspace_context.rs +++ /dev/null @@ -1,283 +0,0 @@ -//! Workspace contexts repository for managing context file metadata. - -use std::future::Future; - -use diesel::prelude::*; -use diesel_async::RunQueryDsl; -use uuid::Uuid; - -use crate::model::{NewWorkspaceContext, UpdateWorkspaceContext, WorkspaceContext}; -use crate::types::{CursorPage, CursorPagination, OffsetPagination, Username}; -use crate::{PgConnection, PgError, PgResult, schema}; - -/// Repository for workspace context database operations. -pub trait WorkspaceContextRepository { - /// Creates a new workspace context record. - fn create_workspace_context( - &mut self, - new_context: NewWorkspaceContext, - ) -> impl Future> + Send; - - /// Finds a context by its unique identifier. - fn find_workspace_context_by_id( - &mut self, - context_id: Uuid, - ) -> impl Future>> + Send; - - /// Finds a context by ID within a specific workspace. - fn find_context_in_workspace( - &mut self, - workspace_id: Uuid, - context_id: Uuid, - ) -> impl Future>> + Send; - - /// Finds a context by slug within a specific workspace, with the handle of - /// the account that created it. - /// - /// Excludes soft-deleted contexts. - fn find_context_in_workspace_by_slug( - &mut self, - workspace_id: Uuid, - slug: &str, - ) -> impl Future>> + Send; - - /// Lists all contexts in a workspace with offset pagination. - fn offset_list_workspace_contexts( - &mut self, - workspace_id: Uuid, - pagination: OffsetPagination, - ) -> impl Future>> + Send; - - /// Lists all contexts in a workspace with cursor pagination, each paired - /// with the handle of the account that created it. - fn cursor_list_workspace_contexts( - &mut self, - workspace_id: Uuid, - pagination: CursorPagination, - ) -> impl Future>> + Send; - - /// Updates a context with new data. - fn update_workspace_context( - &mut self, - context_id: Uuid, - updates: UpdateWorkspaceContext, - ) -> impl Future> + Send; - - /// Soft deletes a context by setting the deletion timestamp. - fn delete_workspace_context( - &mut self, - context_id: Uuid, - ) -> impl Future> + Send; - - /// Counts contexts in a workspace. - fn count_workspace_contexts( - &mut self, - workspace_id: Uuid, - ) -> impl Future> + Send; -} - -impl WorkspaceContextRepository for PgConnection { - async fn create_workspace_context( - &mut self, - new_context: NewWorkspaceContext, - ) -> PgResult { - use schema::workspace_contexts; - - let context = diesel::insert_into(workspace_contexts::table) - .values(&new_context) - .returning(WorkspaceContext::as_returning()) - .get_result(self) - .await - .map_err(PgError::from)?; - - Ok(context) - } - - async fn find_workspace_context_by_id( - &mut self, - context_id: Uuid, - ) -> PgResult> { - use schema::workspace_contexts::{self, dsl}; - - let context = workspace_contexts::table - .filter(dsl::id.eq(context_id)) - .filter(dsl::deleted_at.is_null()) - .select(WorkspaceContext::as_select()) - .first(self) - .await - .optional() - .map_err(PgError::from)?; - - Ok(context) - } - - async fn find_context_in_workspace( - &mut self, - workspace_id: Uuid, - context_id: Uuid, - ) -> PgResult> { - use schema::workspace_contexts::{self, dsl}; - - let context = workspace_contexts::table - .filter(dsl::id.eq(context_id)) - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::deleted_at.is_null()) - .select(WorkspaceContext::as_select()) - .first(self) - .await - .optional() - .map_err(PgError::from)?; - - Ok(context) - } - - async fn find_context_in_workspace_by_slug( - &mut self, - workspace_id: Uuid, - slug: &str, - ) -> PgResult> { - use schema::workspace_contexts::dsl; - use schema::{accounts, workspace_contexts}; - - let context = workspace_contexts::table - .inner_join(accounts::table) - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::slug.eq(slug)) - .filter(dsl::deleted_at.is_null()) - .select((WorkspaceContext::as_select(), accounts::username)) - .first(self) - .await - .optional() - .map_err(PgError::from)?; - - Ok(context) - } - - async fn offset_list_workspace_contexts( - &mut self, - workspace_id: Uuid, - pagination: OffsetPagination, - ) -> PgResult> { - use schema::workspace_contexts::{self, dsl}; - - let contexts = workspace_contexts::table - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::deleted_at.is_null()) - .order(dsl::created_at.desc()) - .limit(pagination.limit) - .offset(pagination.offset) - .select(WorkspaceContext::as_select()) - .load(self) - .await - .map_err(PgError::from)?; - - Ok(contexts) - } - - async fn cursor_list_workspace_contexts( - &mut self, - workspace_id: Uuid, - pagination: CursorPagination, - ) -> PgResult> { - use schema::workspace_contexts::dsl; - use schema::{accounts, workspace_contexts}; - - let total = if pagination.include_count { - Some( - workspace_contexts::table - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::deleted_at.is_null()) - .count() - .get_result::(self) - .await - .map_err(PgError::from)?, - ) - } else { - None - }; - - let query = workspace_contexts::table - .inner_join(accounts::table) - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::deleted_at.is_null()) - .into_boxed(); - - let limit = pagination.limit + 1; - - let items: Vec<(WorkspaceContext, Username)> = if let Some(cursor) = &pagination.after { - let cursor_time = jiff_diesel::Timestamp::from(cursor.timestamp); - - query - .filter( - dsl::created_at - .lt(&cursor_time) - .or(dsl::created_at.eq(&cursor_time).and(dsl::id.lt(cursor.id))), - ) - .select((WorkspaceContext::as_select(), accounts::username)) - .order((dsl::created_at.desc(), dsl::id.desc())) - .limit(limit) - .load(self) - .await - .map_err(PgError::from)? - } else { - query - .select((WorkspaceContext::as_select(), accounts::username)) - .order((dsl::created_at.desc(), dsl::id.desc())) - .limit(limit) - .load(self) - .await - .map_err(PgError::from)? - }; - - Ok(CursorPage::new( - items, - total, - pagination.limit, - |(c, _): &(WorkspaceContext, Username)| (c.created_at.into(), c.id), - )) - } - - async fn update_workspace_context( - &mut self, - context_id: Uuid, - updates: UpdateWorkspaceContext, - ) -> PgResult { - use schema::workspace_contexts::{self, dsl}; - - let context = diesel::update(workspace_contexts::table.filter(dsl::id.eq(context_id))) - .set(&updates) - .returning(WorkspaceContext::as_returning()) - .get_result(self) - .await - .map_err(PgError::from)?; - - Ok(context) - } - - async fn delete_workspace_context(&mut self, context_id: Uuid) -> PgResult<()> { - use diesel::dsl::now; - use schema::workspace_contexts::{self, dsl}; - - diesel::update(workspace_contexts::table.filter(dsl::id.eq(context_id))) - .set(dsl::deleted_at.eq(now)) - .execute(self) - .await - .map_err(PgError::from)?; - - Ok(()) - } - - async fn count_workspace_contexts(&mut self, workspace_id: Uuid) -> PgResult { - use schema::workspace_contexts::{self, dsl}; - - let count = workspace_contexts::table - .filter(dsl::workspace_id.eq(workspace_id)) - .filter(dsl::deleted_at.is_null()) - .count() - .get_result(self) - .await - .map_err(PgError::from)?; - - Ok(count) - } -} diff --git a/crates/nvisy-postgres/src/schema.rs b/crates/nvisy-postgres/src/schema.rs index 16b5e93..313eefd 100644 --- a/crates/nvisy-postgres/src/schema.rs +++ b/crates/nvisy-postgres/src/schema.rs @@ -173,25 +173,6 @@ diesel::table! { } } -diesel::table! { - use diesel::sql_types::*; - - workspace_contexts (id) { - id -> Uuid, - workspace_id -> Uuid, - account_id -> Uuid, - slug -> Text, - name -> Text, - description -> Nullable, - version -> Text, - definition -> Bytea, - metadata -> Jsonb, - created_at -> Timestamptz, - updated_at -> Timestamptz, - deleted_at -> Nullable, - } -} - diesel::table! { use diesel::sql_types::*; use super::sql_types::FileSource; @@ -273,16 +254,6 @@ diesel::table! { } } -diesel::table! { - use diesel::sql_types::*; - - workspace_pipeline_contexts (pipeline_id, context_id) { - workspace_id -> Uuid, - pipeline_id -> Uuid, - context_id -> Uuid, - } -} - diesel::table! { use diesel::sql_types::*; @@ -406,15 +377,12 @@ diesel::joinable!(workspace_connection_runs -> accounts (account_id)); diesel::joinable!(workspace_connection_runs -> workspace_connections (connection_id)); diesel::joinable!(workspace_connections -> accounts (account_id)); diesel::joinable!(workspace_connections -> workspaces (workspace_id)); -diesel::joinable!(workspace_contexts -> accounts (account_id)); -diesel::joinable!(workspace_contexts -> workspaces (workspace_id)); diesel::joinable!(workspace_files -> accounts (account_id)); diesel::joinable!(workspace_files -> workspaces (workspace_id)); diesel::joinable!(workspace_invites -> workspaces (workspace_id)); diesel::joinable!(workspace_members -> workspaces (workspace_id)); diesel::joinable!(workspace_pipeline_artifacts -> workspace_files (file_id)); diesel::joinable!(workspace_pipeline_artifacts -> workspace_pipeline_runs (run_id)); -diesel::joinable!(workspace_pipeline_contexts -> workspaces (workspace_id)); diesel::joinable!(workspace_pipeline_policies -> workspaces (workspace_id)); diesel::joinable!(workspace_pipeline_runs -> accounts (account_id)); diesel::joinable!(workspace_pipeline_runs -> workspace_files (file_id)); @@ -434,12 +402,10 @@ diesel::allow_tables_to_appear_in_same_query!( workspace_activities, workspace_connection_runs, workspace_connections, - workspace_contexts, workspace_files, workspace_invites, workspace_members, workspace_pipeline_artifacts, - workspace_pipeline_contexts, workspace_pipeline_policies, workspace_pipeline_runs, workspace_pipelines, diff --git a/crates/nvisy-postgres/src/types/constraint/mod.rs b/crates/nvisy-postgres/src/types/constraint/mod.rs index 9ef2c2b..2e929fc 100644 --- a/crates/nvisy-postgres/src/types/constraint/mod.rs +++ b/crates/nvisy-postgres/src/types/constraint/mod.rs @@ -26,7 +26,6 @@ mod pipelines; mod workspace_connection_runs; mod workspace_connections; -mod workspace_contexts; mod workspace_policies; use std::fmt; @@ -44,7 +43,6 @@ pub use self::pipelines::WorkspacePipelineConstraints; pub use self::workspace_activities::WorkspaceActivitiesConstraints; pub use self::workspace_connection_runs::WorkspaceConnectionRunConstraints; pub use self::workspace_connections::WorkspaceConnectionConstraints; -pub use self::workspace_contexts::WorkspaceContextConstraints; pub use self::workspace_invites::WorkspaceInviteConstraints; pub use self::workspace_members::WorkspaceMemberConstraints; pub use self::workspace_policies::WorkspacePolicyConstraints; @@ -81,7 +79,6 @@ pub enum ConstraintViolation { WorkspacePipelineReference(WorkspacePipelineReferenceConstraints), WorkspaceConnection(WorkspaceConnectionConstraints), WorkspaceConnectionRun(WorkspaceConnectionRunConstraints), - WorkspaceContext(WorkspaceContextConstraints), WorkspacePolicy(WorkspacePolicyConstraints), } @@ -152,7 +149,6 @@ impl ConstraintViolation { WorkspaceWebhookConstraints::new => WorkspaceWebhook, WorkspaceConnectionRunConstraints::new => WorkspaceConnectionRun, WorkspaceConnectionConstraints::new => WorkspaceConnection, - WorkspaceContextConstraints::new => WorkspaceContext, WorkspacePolicyConstraints::new => WorkspacePolicy, WorkspaceFileConstraints::new => WorkspaceFile, WorkspacePipelineRunConstraints::new => WorkspacePipelineRun, @@ -191,7 +187,6 @@ impl ConstraintViolation { ConstraintViolation::WorkspacePipelineReference(_) => "pipeline_references", ConstraintViolation::WorkspaceConnection(_) => "workspace_connections", ConstraintViolation::WorkspaceConnectionRun(_) => "workspace_connection_runs", - ConstraintViolation::WorkspaceContext(_) => "workspace_contexts", ConstraintViolation::WorkspacePolicy(_) => "workspace_policies", } } @@ -220,7 +215,6 @@ impl ConstraintViolation { ConstraintViolation::WorkspaceConnection(_) | ConstraintViolation::WorkspaceConnectionRun(_) => "connections", - ConstraintViolation::WorkspaceContext(_) => "contexts", ConstraintViolation::WorkspacePolicy(_) => "policies", } } @@ -248,7 +242,6 @@ impl ConstraintViolation { ConstraintViolation::WorkspacePipelineReference(c) => c.categorize(), ConstraintViolation::WorkspaceConnection(c) => c.categorize(), ConstraintViolation::WorkspaceConnectionRun(c) => c.categorize(), - ConstraintViolation::WorkspaceContext(c) => c.categorize(), ConstraintViolation::WorkspacePolicy(c) => c.categorize(), } } @@ -281,7 +274,6 @@ impl fmt::Display for ConstraintViolation { ConstraintViolation::WorkspacePipelineReference(c) => write!(f, "{}", c), ConstraintViolation::WorkspaceConnection(c) => write!(f, "{}", c), ConstraintViolation::WorkspaceConnectionRun(c) => write!(f, "{}", c), - ConstraintViolation::WorkspaceContext(c) => write!(f, "{}", c), ConstraintViolation::WorkspacePolicy(c) => write!(f, "{}", c), } } diff --git a/crates/nvisy-postgres/src/types/constraint/pipeline_references.rs b/crates/nvisy-postgres/src/types/constraint/pipeline_references.rs index 1239b06..973e3d5 100644 --- a/crates/nvisy-postgres/src/types/constraint/pipeline_references.rs +++ b/crates/nvisy-postgres/src/types/constraint/pipeline_references.rs @@ -5,10 +5,10 @@ use strum::{Display, EnumIter, EnumString}; use super::ConstraintCategory; -/// Foreign-key violations on the pipeline → policy / context join tables. +/// Foreign-key violations on the pipeline → policy join table. /// -/// These fire when a pipeline references a policy or context id that does not -/// exist in its workspace, so they map to a client error rather than a 500. +/// These fire when a pipeline references a policy id that does not exist in its +/// workspace, so they map to a client error rather than a 500. #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Serialize, Deserialize, Display, EnumIter, EnumString)] #[serde(into = "String", try_from = "String")] @@ -16,12 +16,8 @@ pub enum WorkspacePipelineReferenceConstraints { // Foreign-key constraints (referenced row must exist in the workspace) #[strum(serialize = "workspace_pipeline_policies_policy_fkey")] PolicyReference, - #[strum(serialize = "workspace_pipeline_contexts_context_fkey")] - ContextReference, #[strum(serialize = "workspace_pipeline_policies_pipeline_fkey")] PolicyPipelineReference, - #[strum(serialize = "workspace_pipeline_contexts_pipeline_fkey")] - ContextPipelineReference, } impl WorkspacePipelineReferenceConstraints { diff --git a/crates/nvisy-postgres/src/types/constraint/workspace_contexts.rs b/crates/nvisy-postgres/src/types/constraint/workspace_contexts.rs deleted file mode 100644 index 73c4da8..0000000 --- a/crates/nvisy-postgres/src/types/constraint/workspace_contexts.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Workspace contexts table constraint violations. - -use serde::{Deserialize, Serialize}; -use strum::{Display, EnumIter, EnumString}; - -use super::ConstraintCategory; - -/// Workspace contexts table constraint violations. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -#[derive(Serialize, Deserialize, Display, EnumIter, EnumString)] -#[serde(into = "String", try_from = "String")] -pub enum WorkspaceContextConstraints { - // Slug validation constraints - #[strum(serialize = "workspace_contexts_slug_length")] - SlugLength, - #[strum(serialize = "workspace_contexts_slug_format")] - SlugFormat, - - // Name validation constraints - #[strum(serialize = "workspace_contexts_name_length")] - NameLength, - - // Description validation constraints - #[strum(serialize = "workspace_contexts_description_length")] - DescriptionLength, - - // Definition validation constraints - #[strum(serialize = "workspace_contexts_definition_size")] - DefinitionSize, - - // Version validation constraints - #[strum(serialize = "workspace_contexts_version_length")] - VersionLength, - - // Metadata validation constraints - #[strum(serialize = "workspace_contexts_metadata_size")] - MetadataSize, - - // Uniqueness constraints - #[strum(serialize = "workspace_contexts_workspace_id_id_key")] - WorkspaceIdIdUnique, - #[strum(serialize = "workspace_contexts_workspace_id_slug_key")] - SlugUnique, - - // Chronological constraints - #[strum(serialize = "workspace_contexts_updated_after_created")] - UpdatedAfterCreated, - #[strum(serialize = "workspace_contexts_deleted_after_created")] - DeletedAfterCreated, -} - -impl WorkspaceContextConstraints { - /// Creates a new [`WorkspaceContextConstraints`] from the constraint name. - pub fn new(constraint: &str) -> Option { - constraint.parse().ok() - } - - /// Returns the category of this constraint violation. - pub fn categorize(&self) -> ConstraintCategory { - match self { - WorkspaceContextConstraints::SlugLength - | WorkspaceContextConstraints::SlugFormat - | WorkspaceContextConstraints::NameLength - | WorkspaceContextConstraints::DescriptionLength - | WorkspaceContextConstraints::DefinitionSize - | WorkspaceContextConstraints::VersionLength - | WorkspaceContextConstraints::MetadataSize => ConstraintCategory::Validation, - - WorkspaceContextConstraints::WorkspaceIdIdUnique - | WorkspaceContextConstraints::SlugUnique => ConstraintCategory::Uniqueness, - - WorkspaceContextConstraints::UpdatedAfterCreated - | WorkspaceContextConstraints::DeletedAfterCreated => ConstraintCategory::Chronological, - } - } -} - -impl From for String { - #[inline] - fn from(val: WorkspaceContextConstraints) -> Self { - val.to_string() - } -} - -impl TryFrom for WorkspaceContextConstraints { - type Error = strum::ParseError; - - #[inline] - fn try_from(value: String) -> Result { - value.parse() - } -} diff --git a/crates/nvisy-postgres/src/types/mod.rs b/crates/nvisy-postgres/src/types/mod.rs index 42410b2..bd87c17 100644 --- a/crates/nvisy-postgres/src/types/mod.rs +++ b/crates/nvisy-postgres/src/types/mod.rs @@ -19,8 +19,8 @@ pub use constraint::{ AccountApiTokenConstraints, AccountConstraints, AccountNotificationConstraints, ConstraintCategory, ConstraintViolation, WorkspaceActivitiesConstraints, WorkspaceConnectionConstraints, WorkspaceConnectionRunConstraints, WorkspaceConstraints, - WorkspaceContextConstraints, WorkspaceFileConstraints, WorkspaceInviteConstraints, - WorkspaceMemberConstraints, WorkspacePipelineArtifactConstraints, WorkspacePipelineConstraints, + WorkspaceFileConstraints, WorkspaceInviteConstraints, WorkspaceMemberConstraints, + WorkspacePipelineArtifactConstraints, WorkspacePipelineConstraints, WorkspacePipelineReferenceConstraints, WorkspacePipelineRunConstraints, WorkspacePolicyConstraints, WorkspaceWebhookConstraints, }; diff --git a/crates/nvisy-server/Cargo.toml b/crates/nvisy-server/Cargo.toml index 959dfaf..f944010 100644 --- a/crates/nvisy-server/Cargo.toml +++ b/crates/nvisy-server/Cargo.toml @@ -29,7 +29,6 @@ default = [] [dependencies] # Runtime crates nvisy-engine = { workspace = true } -nvisy-schema = { workspace = true } # Internal crates nvisy-core = { workspace = true, features = ["schema"] } diff --git a/crates/nvisy-server/src/extract/auth/permission.rs b/crates/nvisy-server/src/extract/auth/permission.rs index c8c80a0..bd97238 100644 --- a/crates/nvisy-server/src/extract/auth/permission.rs +++ b/crates/nvisy-server/src/extract/auth/permission.rs @@ -1,7 +1,7 @@ //! Core authorization types and utilities. //! //! This module provides the fundamental types used for authorization throughout -//! the nvisy system, including permissions, contexts, and results. +//! the nvisy system, including permissions and results. use std::borrow::Cow; @@ -63,12 +63,6 @@ pub enum Permission { /// Can create, modify, and manage workspace connections. ManageConnections, - // Context permissions - /// Can view workspace contexts. - ViewContexts, - /// Can create, modify, and manage workspace contexts. - ManageContexts, - // Policy permissions /// Can view workspace policies. ViewPolicies, @@ -108,7 +102,6 @@ impl Permission { | Self::ViewPipelines | Self::ViewMembers | Self::ViewConnections - | Self::ViewContexts | Self::ViewPolicies | Self::ViewWebhooks => WorkspaceRole::Guest, @@ -127,7 +120,6 @@ impl Permission { | Self::InviteMembers | Self::RemoveMembers | Self::ManageConnections - | Self::ManageContexts | Self::ManagePolicies | Self::CreateWebhooks | Self::UpdateWebhooks diff --git a/crates/nvisy-server/src/handler/contexts.rs b/crates/nvisy-server/src/handler/contexts.rs deleted file mode 100644 index 48a7193..0000000 --- a/crates/nvisy-server/src/handler/contexts.rs +++ /dev/null @@ -1,346 +0,0 @@ -//! Workspace context management handlers. -//! -//! Contexts are structured reference-data documents (the engine's Context -//! type) consumed by the redaction pipeline. The definition is validated -//! against the schema, then stored encrypted (XChaCha20-Poly1305, workspace- -//! derived key) as a JSONB-free BYTEA column in PostgreSQL, scoped to a -//! workspace. - -use aide::axum::ApiRouter; -use aide::transform::TransformOperation; -use axum::extract::State; -use axum::http::StatusCode; -use nvisy_postgres::model::{ - NewWorkspaceContext, UpdateWorkspaceContext, WorkspaceContext as WorkspaceContextModel, -}; -use nvisy_postgres::query::WorkspaceContextRepository; -use nvisy_postgres::types::Username; -use nvisy_postgres::{PgClient, PgConn}; -use uuid::Uuid; - -use crate::extract::{ - AuthProvider, AuthState, Json, Path, Permission, Query, ValidateJson, WorkspaceContext, -}; -use crate::handler::request::{ContextPathParams, CreateContext, CursorPagination, UpdateContext}; -use crate::handler::response::{Context, ContextsPage, ErrorResponse}; -use crate::handler::{Error, Result}; -use crate::service::{CryptoService, ServiceState}; - -/// Tracing target for workspace context operations. -const TRACING_TARGET: &str = "nvisy_server::handler::contexts"; - -/// Creates a new workspace context. -/// -/// The request body carries a structured context definition; its name, -/// description, and version drive the stored record unless overridden. -/// Requires `ManageContexts` permission for the workspace. -#[tracing::instrument( - skip_all, - fields( - account_id = %auth_state.account_id, - workspace_id = %workspace.id, - ) -)] -async fn create_context( - State(pg_client): State, - State(crypto): State, - AuthState(auth_state): AuthState, - WorkspaceContext(workspace): WorkspaceContext, - ValidateJson(request): ValidateJson, -) -> Result<(StatusCode, Json)> { - tracing::debug!(target: TRACING_TARGET, "Creating workspace context"); - - let mut conn = pg_client.get_connection().await?; - - auth_state - .authorize_workspace(&mut conn, workspace.id, Permission::ManageContexts) - .await?; - - let definition = &request.definition; - let name = request.name.unwrap_or_else(|| definition.name.clone()); - let description = request - .description - .or_else(|| definition.description.clone()); - let version = definition.version.to_string(); - let encrypted = crypto.encrypt_json(workspace.id, definition)?; - - let new_context = NewWorkspaceContext { - workspace_id: workspace.id, - account_id: auth_state.account_id, - slug: request.slug, - name, - description, - version, - definition: encrypted, - metadata: None, - }; - - let context = conn.create_workspace_context(new_context).await?; - - tracing::info!(target: TRACING_TARGET, context_slug = %context.slug, "Context created"); - - let (context, creator_username) = - find_context(&mut conn, workspace.id, context.slug.as_str()).await?; - - Ok(( - StatusCode::CREATED, - Json(Context::from_model( - context, - workspace.slug, - creator_username, - &crypto, - )?), - )) -} - -fn create_context_docs(op: TransformOperation) -> TransformOperation { - op.summary("Create context") - .description("Creates a structured reference-data context for the workspace.") - .response::<201, Json>() - .response::<400, Json>() - .response::<401, Json>() - .response::<403, Json>() -} - -/// Lists all contexts for a workspace. -#[tracing::instrument( - skip_all, - fields( - account_id = %auth_state.account_id, - workspace_id = %workspace.id, - ) -)] -async fn list_contexts( - State(pg_client): State, - State(crypto): State, - AuthState(auth_state): AuthState, - WorkspaceContext(workspace): WorkspaceContext, - Query(pagination): Query, -) -> Result<(StatusCode, Json)> { - tracing::debug!(target: TRACING_TARGET, "Listing workspace contexts"); - - let mut conn = pg_client.get_connection().await?; - - auth_state - .authorize_workspace(&mut conn, workspace.id, Permission::ViewContexts) - .await?; - - let page = conn - .cursor_list_workspace_contexts(workspace.id, pagination.into()) - .await?; - - tracing::debug!( - target: TRACING_TARGET, - context_count = page.items.len(), - "Workspace contexts listed", - ); - - let page = ContextsPage::try_from_cursor_page(page, |(model, creator_username)| { - Context::from_model(model, workspace.slug.clone(), creator_username, &crypto) - })?; - - Ok((StatusCode::OK, Json(page))) -} - -fn list_contexts_docs(op: TransformOperation) -> TransformOperation { - op.summary("List contexts") - .description("Returns all contexts for the workspace.") - .response::<200, Json>() - .response::<401, Json>() - .response::<403, Json>() -} - -/// Retrieves a specific workspace context. -#[tracing::instrument( - skip_all, - fields( - account_id = %auth_state.account_id, - workspace_id = %workspace.id, - context_slug = %path_params.context_slug, - ) -)] -async fn read_context( - State(pg_client): State, - State(crypto): State, - AuthState(auth_state): AuthState, - WorkspaceContext(workspace): WorkspaceContext, - Path(path_params): Path, -) -> Result<(StatusCode, Json)> { - tracing::debug!(target: TRACING_TARGET, "Reading workspace context"); - - let mut conn = pg_client.get_connection().await?; - - auth_state - .authorize_workspace(&mut conn, workspace.id, Permission::ViewContexts) - .await?; - - let (context, creator_username) = - find_context(&mut conn, workspace.id, &path_params.context_slug).await?; - - tracing::debug!(target: TRACING_TARGET, "Workspace context read"); - - Ok(( - StatusCode::OK, - Json(Context::from_model( - context, - workspace.slug, - creator_username, - &crypto, - )?), - )) -} - -fn read_context_docs(op: TransformOperation) -> TransformOperation { - op.summary("Get context") - .description("Returns a single context.") - .response::<200, Json>() - .response::<401, Json>() - .response::<403, Json>() - .response::<404, Json>() -} - -/// Updates a workspace context. -/// -/// All fields are optional; replacing the definition replaces the whole -/// context body (and its version). Requires `ManageContexts` permission. -#[tracing::instrument( - skip_all, - fields( - account_id = %auth_state.account_id, - workspace_id = %workspace.id, - context_slug = %path_params.context_slug, - ) -)] -async fn update_context( - State(pg_client): State, - State(crypto): State, - AuthState(auth_state): AuthState, - WorkspaceContext(workspace): WorkspaceContext, - Path(path_params): Path, - ValidateJson(request): ValidateJson, -) -> Result<(StatusCode, Json)> { - tracing::debug!(target: TRACING_TARGET, "Updating workspace context"); - - let mut conn = pg_client.get_connection().await?; - - auth_state - .authorize_workspace(&mut conn, workspace.id, Permission::ManageContexts) - .await?; - - let (existing, _) = find_context(&mut conn, workspace.id, &path_params.context_slug).await?; - - let (version, definition) = match &request.definition { - Some(definition) => { - let encrypted = crypto.encrypt_json(workspace.id, definition)?; - (Some(definition.version.to_string()), Some(encrypted)) - } - None => (None, None), - }; - - let updates = UpdateWorkspaceContext { - name: request.name, - description: request.description, - version, - definition, - ..Default::default() - }; - - conn.update_workspace_context(existing.id, updates).await?; - - let (context, creator_username) = - find_context(&mut conn, workspace.id, &path_params.context_slug).await?; - - tracing::info!(target: TRACING_TARGET, "Context updated"); - - Ok(( - StatusCode::OK, - Json(Context::from_model( - context, - workspace.slug, - creator_username, - &crypto, - )?), - )) -} - -fn update_context_docs(op: TransformOperation) -> TransformOperation { - op.summary("Update context") - .description("Updates context fields. Replacing the definition replaces the whole body.") - .response::<200, Json>() - .response::<400, Json>() - .response::<401, Json>() - .response::<403, Json>() - .response::<404, Json>() -} - -/// Deletes a workspace context. -#[tracing::instrument( - skip_all, - fields( - account_id = %auth_state.account_id, - workspace_id = %workspace.id, - context_slug = %path_params.context_slug, - ) -)] -async fn delete_context( - State(pg_client): State, - AuthState(auth_state): AuthState, - WorkspaceContext(workspace): WorkspaceContext, - Path(path_params): Path, -) -> Result { - tracing::debug!(target: TRACING_TARGET, "Deleting workspace context"); - - let mut conn = pg_client.get_connection().await?; - - auth_state - .authorize_workspace(&mut conn, workspace.id, Permission::ManageContexts) - .await?; - - let (existing, _) = find_context(&mut conn, workspace.id, &path_params.context_slug).await?; - - conn.delete_workspace_context(existing.id).await?; - - tracing::info!(target: TRACING_TARGET, "Context deleted"); - - Ok(StatusCode::NO_CONTENT) -} - -fn delete_context_docs(op: TransformOperation) -> TransformOperation { - op.summary("Delete context") - .description("Soft-deletes the context from the workspace.") - .response::<204, ()>() - .response::<401, Json>() - .response::<403, Json>() - .response::<404, Json>() -} - -/// Finds a context within a workspace by slug, with its creator's handle, or -/// returns a NotFound error. -async fn find_context( - conn: &mut PgConn, - workspace_id: Uuid, - context_slug: &str, -) -> Result<(WorkspaceContextModel, Username)> { - conn.find_context_in_workspace_by_slug(workspace_id, context_slug) - .await? - .ok_or_else(|| Error::not_found("context")) -} - -/// Returns routes for workspace context management. -pub fn routes() -> ApiRouter { - use aide::axum::routing::*; - - ApiRouter::new() - .api_route( - "/workspaces/{workspaceSlug}/contexts/", - post_with(create_context, create_context_docs) - .get_with(list_contexts, list_contexts_docs), - ) - .api_route( - "/workspaces/{workspaceSlug}/contexts/{contextSlug}/", - get_with(read_context, read_context_docs) - .put_with(update_context, update_context_docs) - .delete_with(delete_context, delete_context_docs), - ) - .with_path_items(|item| item.tag("Contexts")) -} diff --git a/crates/nvisy-server/src/handler/error/pg_error.rs b/crates/nvisy-server/src/handler/error/pg_error.rs index cd0bc82..a028309 100644 --- a/crates/nvisy-server/src/handler/error/pg_error.rs +++ b/crates/nvisy-server/src/handler/error/pg_error.rs @@ -32,7 +32,6 @@ impl From for Error<'static> { ConstraintViolation::WorkspacePipelineReference(c) => c.into(), ConstraintViolation::WorkspaceConnection(c) => c.into(), ConstraintViolation::WorkspaceConnectionRun(c) => c.into(), - ConstraintViolation::WorkspaceContext(c) => c.into(), ConstraintViolation::WorkspacePolicy(c) => c.into(), } } diff --git a/crates/nvisy-server/src/handler/error/pg_pipeline.rs b/crates/nvisy-server/src/handler/error/pg_pipeline.rs index 8baf47f..c1a282b 100644 --- a/crates/nvisy-server/src/handler/error/pg_pipeline.rs +++ b/crates/nvisy-server/src/handler/error/pg_pipeline.rs @@ -1,7 +1,7 @@ //! Pipeline-related constraint violation error handlers. use nvisy_postgres::types::{ - WorkspaceConnectionConstraints, WorkspaceConnectionRunConstraints, WorkspaceContextConstraints, + WorkspaceConnectionConstraints, WorkspaceConnectionRunConstraints, WorkspacePipelineArtifactConstraints, WorkspacePipelineConstraints, WorkspacePipelineReferenceConstraints, WorkspacePipelineRunConstraints, WorkspacePolicyConstraints, @@ -88,15 +88,9 @@ impl From for Error<'static> { ErrorKind::BadRequest .with_message("Referenced policy does not exist in this workspace"), ), - WorkspacePipelineReferenceConstraints::ContextReference => ( - "context", - ErrorKind::BadRequest - .with_message("Referenced context does not exist in this workspace"), - ), // The pipeline side of the FK only fails if the pipeline row vanished // mid-transaction, which is a server-side fault rather than bad input. - WorkspacePipelineReferenceConstraints::PolicyPipelineReference - | WorkspacePipelineReferenceConstraints::ContextPipelineReference => { + WorkspacePipelineReferenceConstraints::PolicyPipelineReference => { ("pipeline", ErrorKind::InternalServerError.into_error()) } }; @@ -151,40 +145,6 @@ impl From for Error<'static> { } } -impl From for Error<'static> { - fn from(c: WorkspaceContextConstraints) -> Self { - let error = - match c { - WorkspaceContextConstraints::NameLength => ErrorKind::BadRequest - .with_message("Context name must be between 1 and 255 characters"), - WorkspaceContextConstraints::DescriptionLength => ErrorKind::BadRequest - .with_message("Context description must be at most 4096 characters"), - WorkspaceContextConstraints::DefinitionSize => ErrorKind::BadRequest - .with_message("Context definition size exceeds maximum limit"), - WorkspaceContextConstraints::VersionLength => ErrorKind::BadRequest - .with_message("Context version must be between 1 and 64 characters"), - WorkspaceContextConstraints::MetadataSize => ErrorKind::BadRequest - .with_message("Context metadata size exceeds maximum limit"), - WorkspaceContextConstraints::SlugLength => ErrorKind::BadRequest - .with_message("Context slug must be between 3 and 32 characters long"), - WorkspaceContextConstraints::SlugFormat => ErrorKind::BadRequest.with_message( - "Context slug must be lowercase alphanumeric with single internal dashes", - ), - WorkspaceContextConstraints::SlugUnique => { - ErrorKind::Conflict.with_message("A context with this slug already exists") - } - WorkspaceContextConstraints::WorkspaceIdIdUnique => ErrorKind::Conflict - .with_message("A context with this identifier already exists"), - WorkspaceContextConstraints::UpdatedAfterCreated - | WorkspaceContextConstraints::DeletedAfterCreated => { - ErrorKind::InternalServerError.into_error() - } - }; - - error.with_resource("workspace_context") - } -} - impl From for Error<'static> { fn from(c: WorkspacePolicyConstraints) -> Self { let error = match c { diff --git a/crates/nvisy-server/src/handler/mod.rs b/crates/nvisy-server/src/handler/mod.rs index 9a1fb18..20d595b 100644 --- a/crates/nvisy-server/src/handler/mod.rs +++ b/crates/nvisy-server/src/handler/mod.rs @@ -6,7 +6,6 @@ mod accounts; mod authentication; mod connections; -mod contexts; mod error; mod files; mod invites; @@ -65,9 +64,6 @@ fn private_routes( if is_included(BuiltinModule::Connections) { router = router.merge(connections::routes()); } - if is_included(BuiltinModule::Contexts) { - router = router.merge(contexts::routes()); - } if is_included(BuiltinModule::Invites) { router = router.merge(invites::routes()); } diff --git a/crates/nvisy-server/src/handler/pipelines.rs b/crates/nvisy-server/src/handler/pipelines.rs index af04241..51d1f5c 100644 --- a/crates/nvisy-server/src/handler/pipelines.rs +++ b/crates/nvisy-server/src/handler/pipelines.rs @@ -59,13 +59,12 @@ async fn create_pipeline( .into_parts(workspace.id, auth_state.account_id) .map_err(serialize_error)?; - let (policy_ids, context_ids) = - resolve_references(&mut conn, workspace.id, &references).await?; + let policy_ids = resolve_references(&mut conn, workspace.id, &references).await?; let pipeline = conn .transaction(async |conn| { let pipeline = conn.create_workspace_pipeline(new_pipeline).await?; - replace_references(conn, &pipeline, &policy_ids, &context_ids).await?; + replace_references(conn, &pipeline, &policy_ids).await?; Ok::(pipeline) }) .await?; @@ -75,13 +74,12 @@ async fn create_pipeline( find_pipeline(&mut conn, workspace.id, pipeline.slug.as_str()).await?; // The references were just written from the request, so build the response - // from its slugs directly instead of reading the join tables back. + // from its slugs directly instead of reading the join table back. let response = Pipeline::from_model( pipeline, workspace.slug, creator_username, references.policy_slugs, - references.context_slugs, ) .map_err(serialize_error)?; @@ -189,7 +187,6 @@ async fn get_pipeline( let artifacts = conn.list_workspace_pipeline_artifacts(pipeline.id).await?; let policy_slugs = conn.list_pipeline_policy_slugs(pipeline.id).await?; - let context_slugs = conn.list_pipeline_context_slugs(pipeline.id).await?; let response = Pipeline::from_model_with_artifacts( pipeline, @@ -197,7 +194,6 @@ async fn get_pipeline( creator_username, artifacts, policy_slugs, - context_slugs, ) .map_err(serialize_error)?; @@ -261,9 +257,9 @@ async fn update_pipeline( let pipeline = conn .update_workspace_pipeline(pipeline_id, update_data) .await?; - // Only touch the join tables when the request supplied a definition. - if let Some((policy_ids, context_ids)) = &resolved { - replace_references(conn, &pipeline, policy_ids, context_ids).await?; + // Only touch the join table when the request supplied a definition. + if let Some(policy_ids) = &resolved { + replace_references(conn, &pipeline, policy_ids).await?; } Ok::(pipeline) }) @@ -276,7 +272,6 @@ async fn update_pipeline( workspace.slug, creator_username, references.policy_slugs, - references.context_slugs, ) .map_err(serialize_error)?, // Partial update left the references untouched: read them back. @@ -355,7 +350,7 @@ async fn find_pipeline( .ok_or_else(|| Error::not_found("pipeline")) } -/// Replaces a pipeline's policy and context references in the join tables. +/// Replaces a pipeline's policy references in the join table. /// /// Run inside the same transaction as the pipeline write so the config JSON and /// its references stay consistent. @@ -363,35 +358,28 @@ async fn replace_references( conn: &mut PgConnection, pipeline: &WorkspacePipeline, policy_ids: &[Uuid], - context_ids: &[Uuid], ) -> PgResult<()> { conn.replace_workspace_pipeline_policies(pipeline.workspace_id, pipeline.id, policy_ids) .await?; - conn.replace_workspace_pipeline_contexts(pipeline.workspace_id, pipeline.id, context_ids) - .await?; Ok(()) } -/// Resolves a set of policy and context slugs to their ids within a workspace, -/// rejecting the whole request with a 404 if any slug is unknown. +/// Resolves a set of policy slugs to their ids within a workspace, rejecting the +/// whole request with a 404 if any slug is unknown. async fn resolve_references( conn: &mut PgConnection, workspace_id: Uuid, references: &PipelineReferences, -) -> Result<(Vec, Vec)> { +) -> Result> { let policy_ids = conn .resolve_policy_slugs(workspace_id, &references.policy_slugs) .await? .ok_or_else(|| Error::not_found("policy"))?; - let context_ids = conn - .resolve_context_slugs(workspace_id, &references.context_slugs) - .await? - .ok_or_else(|| Error::not_found("context"))?; - Ok((policy_ids, context_ids)) + Ok(policy_ids) } /// Builds a [`Pipeline`] response, reading the pipeline's (live) references back -/// from the join tables. Used when the caller did not just write them. +/// from the join table. Used when the caller did not just write them. async fn build_response( conn: &mut PgConnection, pipeline: WorkspacePipeline, @@ -399,15 +387,8 @@ async fn build_response( creator_username: Username, ) -> Result { let policy_slugs = conn.list_pipeline_policy_slugs(pipeline.id).await?; - let context_slugs = conn.list_pipeline_context_slugs(pipeline.id).await?; - Pipeline::from_model( - pipeline, - workspace_slug, - creator_username, - policy_slugs, - context_slugs, - ) - .map_err(serialize_error) + Pipeline::from_model(pipeline, workspace_slug, creator_username, policy_slugs) + .map_err(serialize_error) } /// Maps a definition (de)serialization failure to an internal error. diff --git a/crates/nvisy-server/src/handler/request/contexts.rs b/crates/nvisy-server/src/handler/request/contexts.rs deleted file mode 100644 index 96b7754..0000000 --- a/crates/nvisy-server/src/handler/request/contexts.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Context request types. - -use nvisy_postgres::types::Slug; -use nvisy_schema::context::Context as SchemaContext; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use validator::Validate; - -/// Path parameters for context operations. -/// -/// The workspace is resolved by the [`WorkspaceContext`] extractor from the -/// `{workspaceSlug}` path segment. -/// -/// [`WorkspaceContext`]: crate::extract::WorkspaceContext -#[must_use] -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct ContextPathParams { - /// URL slug of the context, unique within its workspace. - pub context_slug: String, -} - -/// Request payload for creating a new workspace context. -/// -/// The `definition` is a structured context the redaction engine consumes; -/// its `name`, `description`, and `version` drive the stored columns unless -/// overridden here. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Validate)] -#[serde(rename_all = "camelCase")] -pub struct CreateContext { - /// Optional display name override. Defaults to the context's own name. - pub name: Option, - /// URL slug, unique within the workspace and immutable after creation. - pub slug: Slug, - /// Optional description override. Defaults to the context's own description. - pub description: Option, - /// The structured context body consumed by the engine. - pub definition: SchemaContext, -} - -/// Request payload for updating an existing workspace context. -/// -/// Replacing the `definition` replaces the whole context body. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Validate)] -#[serde(rename_all = "camelCase")] -pub struct UpdateContext { - /// Human-readable context name. - #[validate(length(min = 1, max = 255))] - pub name: Option, - /// Context description. - #[validate(length(max = 4096))] - pub description: Option>, - /// New context body (replaces the stored definition). - pub definition: Option, -} diff --git a/crates/nvisy-server/src/handler/request/mod.rs b/crates/nvisy-server/src/handler/request/mod.rs index 34511ba..a549e40 100644 --- a/crates/nvisy-server/src/handler/request/mod.rs +++ b/crates/nvisy-server/src/handler/request/mod.rs @@ -3,7 +3,6 @@ mod accounts; mod authentications; mod connections; -mod contexts; mod files; mod invites; mod members; @@ -21,7 +20,6 @@ mod workspaces; pub use accounts::*; pub use authentications::*; pub use connections::*; -pub use contexts::*; pub use files::*; pub use invites::*; pub use members::*; diff --git a/crates/nvisy-server/src/handler/request/pipeline_runs.rs b/crates/nvisy-server/src/handler/request/pipeline_runs.rs index cd369d9..41a4610 100644 --- a/crates/nvisy-server/src/handler/request/pipeline_runs.rs +++ b/crates/nvisy-server/src/handler/request/pipeline_runs.rs @@ -1,7 +1,7 @@ //! Pipeline run request types (detect). +use nvisy_engine::plan::ScopeParams; use nvisy_postgres::types::PipelineRunStatus; -use nvisy_schema::plan::ScopeParams; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/crates/nvisy-server/src/handler/request/pipelines.rs b/crates/nvisy-server/src/handler/request/pipelines.rs index c605192..06d5a85 100644 --- a/crates/nvisy-server/src/handler/request/pipelines.rs +++ b/crates/nvisy-server/src/handler/request/pipelines.rs @@ -4,11 +4,11 @@ //! creation, updates, and filtering. All request types support JSON serialization //! and validation. -use nvisy_postgres::model::{NewWorkspacePipeline, UpdateWorkspacePipeline as UpdatePipelineModel}; -use nvisy_postgres::types::{PipelineStatus, Slug}; -use nvisy_schema::plan::{ +use nvisy_engine::plan::{ DeduplicationParams, EnricherParams, LabelCatalogParams, RecognizerParams, ScopeParams, }; +use nvisy_postgres::model::{NewWorkspacePipeline, UpdateWorkspacePipeline as UpdatePipelineModel}; +use nvisy_postgres::types::{PipelineStatus, Slug}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -26,8 +26,8 @@ use validator::Validate; /// - `recognizers` / `enrichers` / `deduplication` / `label_catalog` — the /// detection machinery, assembled into an engine `AnalyzerParams` per request. /// - `default_scope` — optional pipeline-wide scope a document may override. -/// - `policy_slugs` / `context_slugs` — references to the workspace's policies -/// and contexts, resolved at run time. +/// - `policy_slugs` — references to the workspace's policies, resolved at run +/// time. #[must_use] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, Validate)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -61,31 +61,23 @@ pub struct PipelineDefinition { #[serde(default, skip_serializing_if = "Vec::is_empty")] #[validate(length(max = 64))] pub policy_slugs: Vec, - /// Slugs of workspace contexts supplied to detection. - /// - /// Stored relationally in the `workspace_pipeline_contexts` join table, not the JSON - /// definition; surfaced here so the API exposes one coherent object. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[validate(length(max = 64))] - pub context_slugs: Vec, } impl PipelineDefinition { /// Splits the definition into its stored parts: the engine config JSON (with - /// the relational references removed) and the policy / context reference slugs. + /// the relational references removed) and the policy reference slugs. /// - /// The references live in join tables, so they are stripped from the JSON to + /// The references live in a join table, so they are stripped from the JSON to /// keep a single source of truth. Serialization failure is surfaced rather /// than swallowed so a bad config never gets silently persisted as empty. - pub fn into_parts(mut self) -> serde_json::Result<(serde_json::Value, Vec, Vec)> { + pub fn into_parts(mut self) -> serde_json::Result<(serde_json::Value, Vec)> { let policy_slugs = std::mem::take(&mut self.policy_slugs); - let context_slugs = std::mem::take(&mut self.context_slugs); let config = serde_json::to_value(&self)?; - Ok((config, policy_slugs, context_slugs)) + Ok((config, policy_slugs)) } /// Rebuilds a definition from stored config JSON and the reference slugs read - /// back from the join tables. + /// back from the join table. /// /// Decoding failure is surfaced rather than swallowed: a stored config that /// does not match the schema is a server-side data error, not an empty @@ -93,11 +85,9 @@ impl PipelineDefinition { pub fn from_parts( config: serde_json::Value, policy_slugs: Vec, - context_slugs: Vec, ) -> serde_json::Result { let mut definition: Self = serde_json::from_value(config)?; definition.policy_slugs = policy_slugs; - definition.context_slugs = context_slugs; Ok(definition) } } @@ -125,21 +115,19 @@ pub struct CreatePipeline { } /// A pipeline's reference slugs, split out to be resolved to ids and written to -/// the join tables after the pipeline row exists. +/// the join table after the pipeline row exists. #[derive(Debug, Default, Clone)] pub struct PipelineReferences { /// Slugs of the policies the pipeline references. pub policy_slugs: Vec, - /// Slugs of the contexts the pipeline references. - pub context_slugs: Vec, } impl CreatePipeline { /// Splits this request into the pipeline model and its reference ids. /// - /// The stored model carries only the engine config JSON; the policy and - /// context references are returned separately for the caller to persist into - /// the join tables (`None` when no definition was supplied). + /// The stored model carries only the engine config JSON; the policy + /// references are returned separately for the caller to persist into the + /// join table (`None` when no definition was supplied). /// /// # Arguments /// @@ -175,11 +163,8 @@ impl CreatePipeline { fn split_definition( definition: Option, ) -> serde_json::Result<(serde_json::Value, PipelineReferences)> { - let (config, policy_slugs, context_slugs) = definition.unwrap_or_default().into_parts()?; - let references = PipelineReferences { - policy_slugs, - context_slugs, - }; + let (config, policy_slugs) = definition.unwrap_or_default().into_parts()?; + let references = PipelineReferences { policy_slugs }; Ok((config, references)) } @@ -208,21 +193,15 @@ impl UpdatePipeline { /// Splits this request into the update model and its reference ids. /// /// A missing `definition` leaves both the config column and the reference - /// join tables untouched (partial update); a present one replaces both, so + /// join table untouched (partial update); a present one replaces both, so /// the references are returned only in that case. pub fn into_parts( self, ) -> serde_json::Result<(UpdatePipelineModel, Option)> { let (definition, references) = match self.definition { Some(definition) => { - let (config, policy_slugs, context_slugs) = definition.into_parts()?; - ( - Some(config), - Some(PipelineReferences { - policy_slugs, - context_slugs, - }), - ) + let (config, policy_slugs) = definition.into_parts()?; + (Some(config), Some(PipelineReferences { policy_slugs })) } None => (None, None), }; diff --git a/crates/nvisy-server/src/handler/request/policies.rs b/crates/nvisy-server/src/handler/request/policies.rs index 8ffb5ce..3b66421 100644 --- a/crates/nvisy-server/src/handler/request/policies.rs +++ b/crates/nvisy-server/src/handler/request/policies.rs @@ -1,7 +1,7 @@ //! Policy request types. +use nvisy_engine::policy::Policy as SchemaPolicy; use nvisy_postgres::types::Slug; -use nvisy_schema::policy::Policy as SchemaPolicy; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; diff --git a/crates/nvisy-server/src/handler/response/contexts.rs b/crates/nvisy-server/src/handler/response/contexts.rs deleted file mode 100644 index 440b209..0000000 --- a/crates/nvisy-server/src/handler/response/contexts.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Context response types. - -use jiff::Timestamp; -use nvisy_postgres::model::WorkspaceContext; -use nvisy_postgres::types::{Slug, Username}; -use nvisy_schema::context::Context as SchemaContext; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use super::Page; -use crate::service::CryptoService; - -/// Response type for a workspace context. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct Context { - /// URL slug of the context, unique within its workspace. - pub slug: Slug, - /// Slug of the workspace this context belongs to. - pub workspace_slug: Slug, - /// Handle of the account that created this context. - pub creator_username: Username, - /// Human-readable context name. - pub name: String, - /// Context description. - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// Semver of the context body. - pub version: String, - /// The structured context body consumed by the engine. - pub definition: SchemaContext, - /// When the context was created. - pub created_at: Timestamp, - /// When the context was last updated. - pub updated_at: Timestamp, -} - -/// Paginated list of contexts. -pub type ContextsPage = Page; - -impl Context { - /// Creates a response from a database model, decrypting the definition. - pub fn from_model( - context: WorkspaceContext, - workspace_slug: Slug, - creator_username: Username, - crypto: &CryptoService, - ) -> crate::handler::Result { - let definition = - crypto.decrypt_json::(context.workspace_id, &context.definition)?; - - Ok(Self { - slug: context.slug, - workspace_slug, - creator_username, - name: context.name, - description: context.description, - version: context.version, - definition, - created_at: context.created_at.into(), - updated_at: context.updated_at.into(), - }) - } -} diff --git a/crates/nvisy-server/src/handler/response/mod.rs b/crates/nvisy-server/src/handler/response/mod.rs index 8f8b360..63bce93 100644 --- a/crates/nvisy-server/src/handler/response/mod.rs +++ b/crates/nvisy-server/src/handler/response/mod.rs @@ -9,7 +9,6 @@ mod activities; mod artifacts; mod authentications; mod connections; -mod contexts; mod errors; mod files; mod invites; @@ -28,7 +27,6 @@ pub use activities::*; pub use artifacts::*; pub use authentications::*; pub use connections::*; -pub use contexts::*; pub use errors::*; pub use files::*; pub use invites::*; diff --git a/crates/nvisy-server/src/handler/response/pipelines.rs b/crates/nvisy-server/src/handler/response/pipelines.rs index be9f553..e732294 100644 --- a/crates/nvisy-server/src/handler/response/pipelines.rs +++ b/crates/nvisy-server/src/handler/response/pipelines.rs @@ -41,15 +41,14 @@ pub struct Pipeline { impl Pipeline { /// Creates a response from the database model and its reference slugs. /// - /// The `policy_slugs` / `context_slugs` come from the join tables and are - /// merged with the stored engine config to rebuild the full definition. - /// Fails if the stored config JSON does not decode to the current schema. + /// The `policy_slugs` come from the join table and are merged with the stored + /// engine config to rebuild the full definition. Fails if the stored config + /// JSON does not decode to the current schema. pub fn from_model( pipeline: model::WorkspacePipeline, workspace_slug: Slug, creator_username: Username, policy_slugs: Vec, - context_slugs: Vec, ) -> serde_json::Result { Self::assemble( pipeline, @@ -57,7 +56,6 @@ impl Pipeline { creator_username, Vec::new(), policy_slugs, - context_slugs, ) } @@ -68,7 +66,6 @@ impl Pipeline { creator_username: Username, artifacts: Vec, policy_slugs: Vec, - context_slugs: Vec, ) -> serde_json::Result { let artifacts = artifacts.into_iter().map(Artifact::from_model).collect(); Self::assemble( @@ -77,7 +74,6 @@ impl Pipeline { creator_username, artifacts, policy_slugs, - context_slugs, ) } @@ -88,10 +84,8 @@ impl Pipeline { creator_username: Username, artifacts: Vec, policy_slugs: Vec, - context_slugs: Vec, ) -> serde_json::Result { - let definition = - PipelineDefinition::from_parts(pipeline.definition, policy_slugs, context_slugs)?; + let definition = PipelineDefinition::from_parts(pipeline.definition, policy_slugs)?; Ok(Self { slug: pipeline.slug, workspace_slug, diff --git a/crates/nvisy-server/src/handler/response/policies.rs b/crates/nvisy-server/src/handler/response/policies.rs index b98b17a..5dea47c 100644 --- a/crates/nvisy-server/src/handler/response/policies.rs +++ b/crates/nvisy-server/src/handler/response/policies.rs @@ -1,9 +1,9 @@ //! Policy response types. use jiff::Timestamp; +use nvisy_engine::policy::Policy as SchemaPolicy; use nvisy_postgres::model::WorkspacePolicy; use nvisy_postgres::types::{Slug, Username}; -use nvisy_schema::policy::Policy as SchemaPolicy; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/nvisy-server/src/handler/runs.rs b/crates/nvisy-server/src/handler/runs.rs index 5a84471..4079308 100644 --- a/crates/nvisy-server/src/handler/runs.rs +++ b/crates/nvisy-server/src/handler/runs.rs @@ -13,6 +13,9 @@ use axum::extract::State; use axum::http::{HeaderMap, StatusCode}; use bytes::Bytes; use nvisy_engine::AnalyzedDocument; +use nvisy_engine::file::Document; +use nvisy_engine::plan::{AnalyzerParams, AnyAnnotations, ScopeParams}; +use nvisy_engine::policy::Policy as SchemaPolicy; use nvisy_nats::NatsClient; use nvisy_nats::object::{FileKey, FilesBucket, IntermediateKey, IntermediatesBucket}; use nvisy_postgres::model::{ @@ -21,16 +24,12 @@ use nvisy_postgres::model::{ WorkspacePipelineRun, }; use nvisy_postgres::query::{ - AccountRepository, PipelineReferenceRepository, WorkspaceContextRepository, - WorkspaceFileRepository, WorkspacePipelineArtifactRepository, WorkspacePipelineRepository, + AccountRepository, PipelineReferenceRepository, WorkspaceFileRepository, + WorkspacePipelineArtifactRepository, WorkspacePipelineRepository, WorkspacePipelineRunRepository, WorkspacePolicyRepository, }; use nvisy_postgres::types::{ArtifactType, PipelineRunStatus, Username}; use nvisy_postgres::{PgClient, PgConn}; -use nvisy_schema::context::Context as SchemaContext; -use nvisy_schema::file::Document; -use nvisy_schema::plan::{AnalyzerParams, AnyAnnotations, ScopeParams}; -use nvisy_schema::policy::Policy as SchemaPolicy; use sha2::{Digest, Sha256}; use tokio::io::AsyncReadExt; use uuid::Uuid; @@ -113,8 +112,8 @@ async fn create_pipeline_run( .await? .ok_or_else(|| Error::not_found("file"))?; - let definition = PipelineDefinition::from_parts(pipeline.definition, Vec::new(), Vec::new()) - .map_err(serialize_error)?; + let definition = + PipelineDefinition::from_parts(pipeline.definition, Vec::new()).map_err(serialize_error)?; // Create the run first so its id is the engine correlation id. let new_run = NewWorkspacePipelineRun { @@ -130,9 +129,8 @@ async fn create_pipeline_run( // Assemble the engine inputs and analyze. let document = build_document(&nats, &crypto, &file, run.id).await?; let params = build_analyzer_params(&definition, request.scope); - let contexts = resolve_contexts(&mut conn, &crypto, pipeline.workspace_id, pipeline.id).await?; - let analyzed = match engine.analyze_document(document, ¶ms, &contexts).await { + let analyzed = match engine.analyze_document(document, ¶ms).await { Ok(analyzed) => analyzed, Err(err) => { fail_run(&mut conn, run.id).await; @@ -692,25 +690,6 @@ fn build_analyzer_params( } } -/// Resolves a pipeline's live context references into decrypted engine contexts. -/// -/// Soft-deleted contexts are already filtered out by the repository. -async fn resolve_contexts( - conn: &mut PgConn, - crypto: &CryptoService, - workspace_id: Uuid, - pipeline_id: Uuid, -) -> Result> { - let ids = conn.list_pipeline_context_ids(pipeline_id).await?; - let mut contexts = Vec::with_capacity(ids.len()); - for id in ids { - if let Some(model) = conn.find_context_in_workspace(workspace_id, id).await? { - contexts.push(crypto.decrypt_json::(workspace_id, &model.definition)?); - } - } - Ok(contexts) -} - /// Resolves a pipeline's live policy references into decrypted engine policies. async fn resolve_policies( conn: &mut PgConn, diff --git a/crates/nvisy-server/src/handler/utility/custom_routes.rs b/crates/nvisy-server/src/handler/utility/custom_routes.rs index 1ee7e5a..a9a95e1 100644 --- a/crates/nvisy-server/src/handler/utility/custom_routes.rs +++ b/crates/nvisy-server/src/handler/utility/custom_routes.rs @@ -24,8 +24,6 @@ pub enum BuiltinModule { Workspaces, /// Provider connections. Connections, - /// Detection contexts. - Contexts, /// Workspace invitations. Invites, /// Workspace members. diff --git a/crates/nvisy-server/src/middleware/specification.rs b/crates/nvisy-server/src/middleware/specification.rs index 8b040d0..37014aa 100644 --- a/crates/nvisy-server/src/middleware/specification.rs +++ b/crates/nvisy-server/src/middleware/specification.rs @@ -256,11 +256,6 @@ fn api_docs(api: TransformOpenApi) -> TransformOpenApi { description: Some("Redaction policy configuration".into()), ..Default::default() }) - .tag(Tag { - name: "Contexts".into(), - description: Some("Detection context configuration".into()), - ..Default::default() - }) .tag(Tag { name: "Webhooks".into(), description: Some("Webhook configuration".into()), diff --git a/migrations/2026-01-19-045016_pipelines/down.sql b/migrations/2026-01-19-045014_pipelines/down.sql similarity index 73% rename from migrations/2026-01-19-045016_pipelines/down.sql rename to migrations/2026-01-19-045014_pipelines/down.sql index 168b92b..ec2e2c7 100644 --- a/migrations/2026-01-19-045016_pipelines/down.sql +++ b/migrations/2026-01-19-045014_pipelines/down.sql @@ -1,9 +1,5 @@ -- Revert pipeline tables --- Pipeline → policy / context join tables (drop before their parents) -DROP TABLE IF EXISTS workspace_pipeline_contexts; -DROP TABLE IF EXISTS workspace_pipeline_policies; - -- Artifacts DROP TABLE IF EXISTS workspace_pipeline_artifacts; DROP TYPE IF EXISTS ARTIFACT_TYPE; diff --git a/migrations/2026-01-19-045016_pipelines/up.sql b/migrations/2026-01-19-045014_pipelines/up.sql similarity index 85% rename from migrations/2026-01-19-045016_pipelines/up.sql rename to migrations/2026-01-19-045014_pipelines/up.sql index 03f0b1f..6105d31 100644 --- a/migrations/2026-01-19-045016_pipelines/up.sql +++ b/migrations/2026-01-19-045014_pipelines/up.sql @@ -1,5 +1,5 @@ --- Pipelines: redaction pipeline definitions, their policy/context --- references (join tables), runs, and artifacts. +-- Pipelines: redaction pipeline definitions, their runs, and artifacts. +-- Policy references live in a join table declared alongside policies. -- Pipeline status enum CREATE TYPE PIPELINE_STATUS AS ENUM ( @@ -62,8 +62,8 @@ CREATE TABLE workspace_pipelines ( -- Engine detection + redaction config (nvisy_schema plan as JSON): -- recognizers, enrichers, deduplication, label catalog, default scope. - -- Policy/context references are relational (see the join tables below), - -- not embedded here. + -- Policy references are relational (workspace_pipeline_policies, declared + -- alongside policies), not embedded here. definition JSONB NOT NULL, CONSTRAINT workspace_pipelines_definition_size CHECK (length(definition::TEXT) BETWEEN 2 AND 1048576), @@ -130,46 +130,6 @@ COMMENT ON COLUMN workspace_pipelines.created_at IS 'Creation timestamp'; COMMENT ON COLUMN workspace_pipelines.updated_at IS 'Last modification timestamp'; COMMENT ON COLUMN workspace_pipelines.deleted_at IS 'Soft deletion timestamp'; --- Pipeline → policy references (redaction rules applied by the pipeline). --- The shared workspace_id in both composite foreign keys enforces that a --- pipeline can only reference policies from its own workspace. -CREATE TABLE workspace_pipeline_policies ( - workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, - pipeline_id UUID NOT NULL, - policy_id UUID NOT NULL, - - PRIMARY KEY (pipeline_id, policy_id), - - CONSTRAINT workspace_pipeline_policies_pipeline_fkey FOREIGN KEY (workspace_id, pipeline_id) - REFERENCES workspace_pipelines (workspace_id, id) ON DELETE CASCADE, - CONSTRAINT workspace_pipeline_policies_policy_fkey FOREIGN KEY (workspace_id, policy_id) - REFERENCES workspace_policies (workspace_id, id) ON DELETE CASCADE -); - -CREATE INDEX workspace_pipeline_policies_policy_idx ON workspace_pipeline_policies (policy_id); - -COMMENT ON TABLE workspace_pipeline_policies IS - 'Policies a pipeline applies at redaction. CASCADE cleans up on hard delete.'; - --- Pipeline → context references (reference data supplied to detection). -CREATE TABLE workspace_pipeline_contexts ( - workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, - pipeline_id UUID NOT NULL, - context_id UUID NOT NULL, - - PRIMARY KEY (pipeline_id, context_id), - - CONSTRAINT workspace_pipeline_contexts_pipeline_fkey FOREIGN KEY (workspace_id, pipeline_id) - REFERENCES workspace_pipelines (workspace_id, id) ON DELETE CASCADE, - CONSTRAINT workspace_pipeline_contexts_context_fkey FOREIGN KEY (workspace_id, context_id) - REFERENCES workspace_contexts (workspace_id, id) ON DELETE CASCADE -); - -CREATE INDEX workspace_pipeline_contexts_context_idx ON workspace_pipeline_contexts (context_id); - -COMMENT ON TABLE workspace_pipeline_contexts IS - 'Contexts a pipeline supplies to detection. CASCADE cleans up on hard delete.'; - -- Pipeline runs table (execution instances) CREATE TABLE workspace_pipeline_runs ( -- Primary identifier diff --git a/migrations/2026-01-19-045014_policies/down.sql b/migrations/2026-01-19-045014_policies/down.sql deleted file mode 100644 index ea391ab..0000000 --- a/migrations/2026-01-19-045014_policies/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Revert policies - -DROP TABLE IF EXISTS workspace_policies; diff --git a/migrations/2026-01-19-045015_contexts/down.sql b/migrations/2026-01-19-045015_contexts/down.sql deleted file mode 100644 index 21a5211..0000000 --- a/migrations/2026-01-19-045015_contexts/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Revert contexts - -DROP TABLE IF EXISTS workspace_contexts; diff --git a/migrations/2026-01-19-045015_contexts/up.sql b/migrations/2026-01-19-045015_contexts/up.sql deleted file mode 100644 index 5f1ae89..0000000 --- a/migrations/2026-01-19-045015_contexts/up.sql +++ /dev/null @@ -1,84 +0,0 @@ --- Contexts: structured reference-data definitions (nvisy_schema Context) --- the engine consults. A standalone workspace resource. - --- Workspace contexts table (structured reference-data for redaction) --- The definition holds a nvisy_schema::context::Context the engine consumes. -CREATE TABLE workspace_contexts ( - -- Primary identifier - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- References - workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, - account_id UUID NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, - - -- Composite key target for workspace-scoped foreign keys (join tables). - CONSTRAINT workspace_contexts_workspace_id_id_key UNIQUE (workspace_id, id), - - -- URL identity, unique within the workspace: lowercase alphanumeric with - -- single internal dashes, 3-32 characters. - slug TEXT NOT NULL, - CONSTRAINT workspace_contexts_workspace_id_slug_key UNIQUE (workspace_id, slug), - CONSTRAINT workspace_contexts_slug_length CHECK (length(slug) BETWEEN 3 AND 32), - CONSTRAINT workspace_contexts_slug_format CHECK (slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$'), - - -- Core attributes - name TEXT NOT NULL, - description TEXT DEFAULT NULL, - version TEXT NOT NULL, - - CONSTRAINT workspace_contexts_name_length CHECK (length(trim(name)) BETWEEN 1 AND 255), - CONSTRAINT workspace_contexts_description_length CHECK (description IS NULL OR length(description) <= 4096), - CONSTRAINT workspace_contexts_version_length CHECK (length(trim(version)) BETWEEN 1 AND 64), - - -- Context body (nvisy_schema::context::Context as JSON: typed - -- reference-data entries — biometric, geospatial, temporal, ...). - -- Stored XChaCha20-Poly1305 encrypted with the workspace-derived key. - definition BYTEA NOT NULL, - - CONSTRAINT workspace_contexts_definition_size CHECK (length(definition) BETWEEN 1 AND 1048576), - - -- Metadata (for filtering/display) - metadata JSONB NOT NULL DEFAULT '{}', - - CONSTRAINT workspace_contexts_metadata_size CHECK (length(metadata::TEXT) BETWEEN 2 AND 65536), - - -- Lifecycle timestamps - created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, - updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, - deleted_at TIMESTAMPTZ DEFAULT NULL, - - CONSTRAINT workspace_contexts_updated_after_created CHECK (updated_at >= created_at), - CONSTRAINT workspace_contexts_deleted_after_created CHECK (deleted_at IS NULL OR deleted_at >= created_at) -); - --- Triggers -SELECT setup_updated_at('workspace_contexts'); - --- Indexes -CREATE INDEX workspace_contexts_workspace_idx - ON workspace_contexts (workspace_id, created_at DESC) - WHERE deleted_at IS NULL; - -CREATE INDEX workspace_contexts_account_idx - ON workspace_contexts (account_id, created_at DESC) - WHERE deleted_at IS NULL; - -CREATE UNIQUE INDEX workspace_contexts_name_unique_idx - ON workspace_contexts (workspace_id, lower(trim(name))) - WHERE deleted_at IS NULL; - --- Comments -COMMENT ON TABLE workspace_contexts IS - 'Structured reference-data contexts (nvisy_schema Context) consumed by the engine.'; - -COMMENT ON COLUMN workspace_contexts.id IS 'Unique context identifier'; -COMMENT ON COLUMN workspace_contexts.workspace_id IS 'Parent workspace reference'; -COMMENT ON COLUMN workspace_contexts.account_id IS 'Creator account reference'; -COMMENT ON COLUMN workspace_contexts.name IS 'Human-readable context name (1-255 chars)'; -COMMENT ON COLUMN workspace_contexts.description IS 'Context description (up to 4096 chars)'; -COMMENT ON COLUMN workspace_contexts.version IS 'Semver of the context body'; -COMMENT ON COLUMN workspace_contexts.definition IS 'Encrypted context body (XChaCha20-Poly1305, workspace-derived key)'; -COMMENT ON COLUMN workspace_contexts.metadata IS 'Metadata for filtering/display'; -COMMENT ON COLUMN workspace_contexts.created_at IS 'Creation timestamp'; -COMMENT ON COLUMN workspace_contexts.updated_at IS 'Last modification timestamp'; -COMMENT ON COLUMN workspace_contexts.deleted_at IS 'Soft deletion timestamp'; diff --git a/migrations/2026-01-19-045015_policies/down.sql b/migrations/2026-01-19-045015_policies/down.sql new file mode 100644 index 0000000..086f41b --- /dev/null +++ b/migrations/2026-01-19-045015_policies/down.sql @@ -0,0 +1,6 @@ +-- Revert policies + +-- Pipeline → policy join table (drop before its parent policy table). +DROP TABLE IF EXISTS workspace_pipeline_policies; + +DROP TABLE IF EXISTS workspace_policies; diff --git a/migrations/2026-01-19-045014_policies/up.sql b/migrations/2026-01-19-045015_policies/up.sql similarity index 78% rename from migrations/2026-01-19-045014_policies/up.sql rename to migrations/2026-01-19-045015_policies/up.sql index 3f15236..41de538 100644 --- a/migrations/2026-01-19-045014_policies/up.sql +++ b/migrations/2026-01-19-045015_policies/up.sql @@ -83,3 +83,25 @@ COMMENT ON COLUMN workspace_policies.created_at IS 'Creation timestamp'; COMMENT ON COLUMN workspace_policies.updated_at IS 'Last modification timestamp'; COMMENT ON COLUMN workspace_policies.deleted_at IS 'Soft deletion timestamp'; +-- Pipeline → policy references (redaction rules applied by the pipeline). +-- Lives here (after both workspace_pipelines and workspace_policies exist); the +-- shared workspace_id in both composite foreign keys enforces that a pipeline +-- can only reference policies from its own workspace. +CREATE TABLE workspace_pipeline_policies ( + workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, + pipeline_id UUID NOT NULL, + policy_id UUID NOT NULL, + + PRIMARY KEY (pipeline_id, policy_id), + + CONSTRAINT workspace_pipeline_policies_pipeline_fkey FOREIGN KEY (workspace_id, pipeline_id) + REFERENCES workspace_pipelines (workspace_id, id) ON DELETE CASCADE, + CONSTRAINT workspace_pipeline_policies_policy_fkey FOREIGN KEY (workspace_id, policy_id) + REFERENCES workspace_policies (workspace_id, id) ON DELETE CASCADE +); + +CREATE INDEX workspace_pipeline_policies_policy_idx ON workspace_pipeline_policies (policy_id); + +COMMENT ON TABLE workspace_pipeline_policies IS + 'Policies a pipeline applies at redaction. CASCADE cleans up on hard delete.'; +