feat(migrations): canonical nav schema + apps seed + menu-items template
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) <noreply@anthropic.com>
This commit is contained in:
30
migrations/nav-apps-seed.up.sql
Normal file
30
migrations/nav-apps-seed.up.sql
Normal file
@@ -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();
|
||||
70
migrations/nav-menu-items-template.sql
Normal file
70
migrations/nav-menu-items-template.sql
Normal file
@@ -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;
|
||||
117
migrations/nav-schema.up.sql
Normal file
117
migrations/nav-schema.up.sql
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user