From 960dfeba7c1311a08b1e12b7cc7c97cf60775bb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 11:24:46 +0200 Subject: [PATCH] feat(migrations): canonical nav schema + apps seed + menu-items template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three SQL files for apps to copy into their own migrations directory: - nav-schema.up.sql — schema "nav" + enum menu_type + tables menu_items, menu_role_requirements, menu_permission_requirements, menu_product_requirements, apps. Apply verbatim; the kit owns it. - nav-apps-seed.up.sql — canonical cross-app browse-apps list, idempotent INSERT … ON CONFLICT DO UPDATE. Updates flow via kit version bumps; apps re-apply to receive new entries. - nav-menu-items-template.sql — TEMPLATE only. Each app copies once, renames to its next migration number, and edits the seed rows for its own sidebar/ topbar/subbar items. Adoption pattern documented in the kit README. Co-Authored-By: Claude Opus 4.7 (1M context) --- migrations/nav-apps-seed.up.sql | 30 +++++++ migrations/nav-menu-items-template.sql | 70 +++++++++++++++ migrations/nav-schema.up.sql | 117 +++++++++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 migrations/nav-apps-seed.up.sql create mode 100644 migrations/nav-menu-items-template.sql create mode 100644 migrations/nav-schema.up.sql diff --git a/migrations/nav-apps-seed.up.sql b/migrations/nav-apps-seed.up.sql new file mode 100644 index 0000000..91eb9bf --- /dev/null +++ b/migrations/nav-apps-seed.up.sql @@ -0,0 +1,30 @@ +-- @gsc/web-kit nav.apps canonical seed (apply verbatim, never edit) +-- +-- Replicated into every app's nav.apps table so the "browse apps" panel +-- shows the same canonical list everywhere. Updates flow via kit version +-- bumps: re-copy this file into each app's migrations directory and re-apply. +-- +-- ON CONFLICT (key) DO UPDATE keeps the canonical list in sync — any local +-- edits to these rows will be overwritten on next apply. Add new apps by +-- editing this file in the kit and bumping the kit's minor version. + +INSERT INTO nav.apps (key, name, description, url, icon_class, icon_url, icon_bg, sort_order, enabled) +VALUES + ('gsc-crm', 'CRM', 'Customer relationship management', 'https://crm.gosec.internal', NULL, 'https://assets.gosec.cloud/logos/crm.svg', NULL, 10, TRUE), + ('gsc-chronos', 'Chronos', 'Time tracking and timesheets', 'https://chronos.gosec.internal', NULL, 'https://assets.gosec.cloud/logos/chronos.svg', NULL, 20, TRUE), + ('gsc-meet', 'GSC Meet', 'Video conferencing with AI features', '/apps/gsc-meet', NULL, '/images/demo/logos/1.svg', NULL, 30, TRUE), + ('gsc-voice', 'Voice', 'PBX and telephony management', '/apps/gsc-voice', 'ph-phone', NULL, 'bg-primary-lt', 40, TRUE), + ('gsc-ai-hub', 'AI Hub', 'AI models and services management', '/apps/gsc-ai-hub', NULL, '/images/demo/logos/2.svg', NULL, 50, TRUE), + ('gsc-surveillance', 'Surveillance', 'Video surveillance and security', '/apps/surveillance', NULL, '/images/demo/logos/3.svg', NULL, 60, TRUE), + ('gsc-archive', 'Archive', 'Email archiving and eDiscovery', '/apps/gsc-archive', 'ph-archive-box', NULL, 'bg-info-lt', 70, TRUE), + ('gsc-dam', 'Digital Asset Manager', 'Media and asset management platform', '/apps/gsc-dam', NULL, '/images/demo/logos/4.svg', NULL, 80, TRUE) +ON CONFLICT (key) DO UPDATE + SET name = EXCLUDED.name, + description = EXCLUDED.description, + url = EXCLUDED.url, + icon_class = EXCLUDED.icon_class, + icon_url = EXCLUDED.icon_url, + icon_bg = EXCLUDED.icon_bg, + sort_order = EXCLUDED.sort_order, + enabled = EXCLUDED.enabled, + updated_at = NOW(); diff --git a/migrations/nav-menu-items-template.sql b/migrations/nav-menu-items-template.sql new file mode 100644 index 0000000..02068d3 --- /dev/null +++ b/migrations/nav-menu-items-template.sql @@ -0,0 +1,70 @@ +-- @gsc/web-kit nav.menu_items TEMPLATE — copy once, then adapt per app +-- +-- Each app curates its own sidebar/topbar/subbar items. Copy this file into +-- your app's migrations directory (e.g. apps/gscFoo/migrations/00X_nav_menu_items.up.sql), +-- replace the example rows below with your app's real menu, and apply. +-- +-- The kit will NOT update this file in subsequent versions — once you've +-- copied and adapted it, it's yours. The DDL (nav-schema.up.sql) and the +-- canonical apps list (nav-apps-seed.up.sql) are kit-owned and re-copied +-- on every kit upgrade; menu_items rows belong to the app. +-- +-- Notes on columns: +-- - key: unique stable identifier (used by ON CONFLICT) +-- - translation_key: dotted i18n key, e.g. "menu.dashboard" +-- (resolved via next-intl namespace "menu") +-- - url: "/dashboard" — pass "#" for parent items with children +-- - icon: Phosphor CSS class, e.g. "ph-house" +-- - menu_type: 'sidebar' (left nav), 'topbar' (user dropdown), 'subbar' (settings dropdown) +-- - parent_id: UUID of parent menu_item for nested items (or NULL) +-- - sort_order: ascending; render order within type/parent +-- - is_system_required: cannot be hidden/removed by user customization + +-- ============================================================================ +-- SIDEBAR (left navigation) +-- ============================================================================ +INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order, is_active, is_system_required) +VALUES + -- Replace these example rows with your app's sidebar items. + ('sidebar', 'dashboard', 'menu.dashboard', '/dashboard', 'ph-house', 10, TRUE, TRUE), + ('sidebar', 'example-1', 'menu.example1', '/example-1', 'ph-folder-open', 20, TRUE, FALSE), + ('sidebar', 'example-2', 'menu.example2', '/example-2', 'ph-list', 30, TRUE, FALSE) +ON CONFLICT (key) DO NOTHING; + +-- ============================================================================ +-- TOPBAR (user dropdown in navbar) +-- ============================================================================ +INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order, is_active, is_system_required) +VALUES + ('topbar', 'profile', 'menu.profile', '/profile', 'ph-user', 10, TRUE, TRUE), + ('topbar', 'settings', 'menu.settings', '/settings', 'ph-gear', 20, TRUE, TRUE) +ON CONFLICT (key) DO NOTHING; + +-- ============================================================================ +-- SUBBAR (Settings dropdown items) +-- ============================================================================ +-- Uncomment / customize if your app exposes a Settings dropdown in the subbar. +-- +-- INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order, is_active, is_system_required) +-- VALUES +-- ('subbar', 'general-settings', 'menu.generalSettings', '/settings/general', 'ph-sliders', 10, TRUE, FALSE), +-- ('subbar', 'notifications', 'menu.notifications', '/settings/notifications', 'ph-bell', 20, TRUE, FALSE) +-- ON CONFLICT (key) DO NOTHING; + +-- ============================================================================ +-- NESTED EXAMPLES (parent + children) +-- ============================================================================ +-- For nested sidebar items, set url='#' on the parent and reference its id +-- via parent_id on children. Example: +-- +-- WITH parent AS ( +-- INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order) +-- VALUES ('sidebar', 'reports', 'menu.reports', '#', 'ph-chart-bar', 90) +-- ON CONFLICT (key) DO UPDATE SET sort_order = EXCLUDED.sort_order +-- RETURNING id +-- ) +-- INSERT INTO nav.menu_items (menu_type, parent_id, key, translation_key, url, icon, sort_order) +-- SELECT 'sidebar', parent.id, 'reports-sales', 'menu.reportsSales', '/reports/sales', 'ph-chart-line', 10 FROM parent +-- UNION ALL +-- SELECT 'sidebar', parent.id, 'reports-leads', 'menu.reportsLeads', '/reports/leads', 'ph-chart-pie', 20 FROM parent +-- ON CONFLICT (key) DO NOTHING; diff --git a/migrations/nav-schema.up.sql b/migrations/nav-schema.up.sql new file mode 100644 index 0000000..20e5a70 --- /dev/null +++ b/migrations/nav-schema.up.sql @@ -0,0 +1,117 @@ +-- @gsc/web-kit nav schema (canonical, apply verbatim, never edit) +-- +-- Creates the navigation tables consumed by @gsc/web-kit/chrome AdminShell: +-- - nav.menu_items — per-app sidebar/topbar/subbar items +-- - nav.menu_role_requirements — role gating (enforcement TBD) +-- - nav.menu_permission_requirements +-- - nav.menu_product_requirements +-- - nav.apps — cross-app "browse apps" panel data +-- +-- Per-app philosophy: each app applies this schema to its OWN database +-- (gsc_crm, gsc_chronos, gsc_admin, ...). Menu items are app-specific; +-- the apps list is replicated from nav-apps-seed.up.sql so every app's +-- "browse apps" panel shows the same canonical list. + +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE SCHEMA IF NOT EXISTS nav; + +-- --------------------------------------------------------------------------- +-- Enums +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'menu_type' AND n.nspname = 'nav' + ) THEN + CREATE TYPE nav.menu_type AS ENUM ('sidebar', 'topbar', 'subbar'); + END IF; +END$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'requirement_logic' AND n.nspname = 'nav' + ) THEN + CREATE TYPE nav.requirement_logic AS ENUM ('OR', 'AND'); + END IF; +END$$; + +-- --------------------------------------------------------------------------- +-- menu_items +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS nav.menu_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + menu_type nav.menu_type NOT NULL, + parent_id UUID NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE, + key VARCHAR(64) NOT NULL UNIQUE, + translation_key VARCHAR(128) NOT NULL, + url VARCHAR(256) NOT NULL, + icon VARCHAR(64) NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_system_required BOOLEAN NOT NULL DEFAULT FALSE, + role_logic nav.requirement_logic NOT NULL DEFAULT 'OR', + permission_logic nav.requirement_logic NOT NULL DEFAULT 'OR', + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_menu_items_type ON nav.menu_items (menu_type); +CREATE INDEX IF NOT EXISTS idx_menu_items_parent ON nav.menu_items (parent_id); +CREATE INDEX IF NOT EXISTS idx_menu_items_sort ON nav.menu_items (sort_order); + +-- --------------------------------------------------------------------------- +-- menu requirement tables (role / permission / product) +-- Cross-DB IDs stored as plain strings; no cross-DB FKs. +-- Enforcement is not yet wired in v0.4.0 chrome. +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS nav.menu_role_requirements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + menu_item_id UUID NOT NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE, + role_id VARCHAR(128) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (menu_item_id, role_id) +); + +CREATE TABLE IF NOT EXISTS nav.menu_permission_requirements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + menu_item_id UUID NOT NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE, + permission_id VARCHAR(128) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (menu_item_id, permission_id) +); + +CREATE TABLE IF NOT EXISTS nav.menu_product_requirements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + menu_item_id UUID NOT NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE, + product_id VARCHAR(128) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (menu_item_id, product_id) +); + +-- --------------------------------------------------------------------------- +-- apps — cross-app browse-apps panel data +-- Either icon_class (Phosphor / FA class) or icon_url (logo image) may be set. +-- icon_bg styles the colored square behind icon_class (e.g. 'bg-primary-lt'). +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS nav.apps ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + key VARCHAR(64) NOT NULL UNIQUE, + name VARCHAR(128) NOT NULL, + description VARCHAR(256) NOT NULL DEFAULT '', + url VARCHAR(512) NOT NULL, + icon_class VARCHAR(64) NULL, + icon_url VARCHAR(512) NULL, + icon_bg VARCHAR(32) NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_apps_sort_order ON nav.apps (sort_order);