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>
118 lines
5.1 KiB
SQL
118 lines
5.1 KiB
SQL
-- @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);
|