Initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE osint.cases (
|
||||
id TEXT PRIMARY KEY,
|
||||
state_json JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON SCHEMA osint IS 'GUPI OSINT Board application data';
|
||||
COMMENT ON TABLE osint.cases IS 'Server-authoritative investigation board state';
|
||||
@@ -0,0 +1,130 @@
|
||||
CREATE TABLE osint.levels (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
subtitle TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE osint.widgets (
|
||||
id TEXT PRIMARY KEY,
|
||||
level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE,
|
||||
widget_type TEXT NOT NULL CHECK (widget_type IN ('document', 'evidence', 'note', 'event')),
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
source_widget_id TEXT REFERENCES osint.widgets(id) ON DELETE SET NULL,
|
||||
source_region_key TEXT,
|
||||
event_date DATE,
|
||||
x DOUBLE PRECISION,
|
||||
y DOUBLE PRECISION,
|
||||
width DOUBLE PRECISION,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (level_id, id)
|
||||
);
|
||||
|
||||
CREATE INDEX widgets_level_type_idx ON osint.widgets(level_id, widget_type, sort_order);
|
||||
|
||||
CREATE TABLE osint.widget_regions (
|
||||
id TEXT PRIMARY KEY,
|
||||
document_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE,
|
||||
region_key TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
excerpt TEXT NOT NULL,
|
||||
event_date DATE,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (document_widget_id, region_key)
|
||||
);
|
||||
|
||||
CREATE TABLE osint.level_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE,
|
||||
from_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE,
|
||||
to_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE osint.playthroughs (
|
||||
id TEXT PRIMARY KEY,
|
||||
level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'completed', 'abandoned')),
|
||||
viewport JSONB NOT NULL DEFAULT '{"x":0,"y":28,"zoom":0.7}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE osint.playthrough_widget_state (
|
||||
playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE,
|
||||
widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE,
|
||||
x DOUBLE PRECISION NOT NULL,
|
||||
y DOUBLE PRECISION NOT NULL,
|
||||
width DOUBLE PRECISION NOT NULL,
|
||||
hidden BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
annotation TEXT,
|
||||
PRIMARY KEY (playthrough_id, widget_id)
|
||||
);
|
||||
|
||||
CREATE TABLE osint.playthrough_widgets (
|
||||
id TEXT PRIMARY KEY,
|
||||
playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE,
|
||||
widget_type TEXT NOT NULL CHECK (widget_type IN ('evidence', 'note', 'event')),
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
source_widget_id TEXT REFERENCES osint.widgets(id) ON DELETE SET NULL,
|
||||
source_region_key TEXT,
|
||||
event_date DATE,
|
||||
x DOUBLE PRECISION NOT NULL,
|
||||
y DOUBLE PRECISION NOT NULL,
|
||||
width DOUBLE PRECISION NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE osint.playthrough_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE,
|
||||
from_widget_id TEXT NOT NULL,
|
||||
to_widget_id TEXT NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
-- Preserve levels saved by the pre-widget architecture, if any exist.
|
||||
INSERT INTO osint.levels (id, title, subtitle, status, created_at, updated_at)
|
||||
SELECT id, state_json->>'title', COALESCE(state_json->>'subtitle', ''), 'draft', created_at, updated_at
|
||||
FROM osint.cases;
|
||||
|
||||
INSERT INTO osint.widgets (id, level_id, widget_type, title, content, config, event_date, sort_order)
|
||||
SELECT doc->>'id', c.id, 'document', doc->>'title', '',
|
||||
jsonb_build_object('kind', doc->>'kind', 'date', doc->>'date', 'body', COALESCE(doc->'body', '[]'::jsonb)),
|
||||
NULLIF(doc->>'date', '')::date, ordinality::integer
|
||||
FROM osint.cases c
|
||||
CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'documents') WITH ORDINALITY AS item(doc, ordinality);
|
||||
|
||||
INSERT INTO osint.widget_regions (id, document_widget_id, region_key, label, excerpt, event_date, sort_order)
|
||||
SELECT (doc->>'id') || ':' || (region->>'id'), doc->>'id', region->>'id', region->>'label', region->>'excerpt',
|
||||
NULLIF(region->>'date', '')::date, region_ordinality::integer
|
||||
FROM osint.cases c
|
||||
CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'documents') AS docs(doc)
|
||||
CROSS JOIN LATERAL jsonb_array_elements(doc->'regions') WITH ORDINALITY AS regions(region, region_ordinality);
|
||||
|
||||
INSERT INTO osint.widgets (id, level_id, widget_type, title, content, source_widget_id, source_region_key, event_date, x, y, width, sort_order)
|
||||
SELECT ev->>'id', c.id, ev->>'type', ev->>'title', ev->>'content', NULLIF(ev->>'sourceDocumentId', ''),
|
||||
NULLIF(ev->>'sourceRegionId', ''), NULLIF(ev->>'eventDate', '')::date,
|
||||
(ev->>'x')::double precision, (ev->>'y')::double precision, (ev->>'width')::double precision, ordinality::integer
|
||||
FROM osint.cases c
|
||||
CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'evidence') WITH ORDINALITY AS item(ev, ordinality);
|
||||
|
||||
INSERT INTO osint.level_connections (id, level_id, from_widget_id, to_widget_id)
|
||||
SELECT conn->>'id', c.id, conn->>'fromEvidenceId', conn->>'toEvidenceId'
|
||||
FROM osint.cases c
|
||||
CROSS JOIN LATERAL jsonb_array_elements(c.state_json->'connections') AS connections(conn);
|
||||
|
||||
INSERT INTO osint.playthroughs (id, level_id, viewport)
|
||||
SELECT 'default:' || id, id, COALESCE(state_json->'viewport', '{"x":0,"y":28,"zoom":0.7}'::jsonb)
|
||||
FROM osint.cases;
|
||||
|
||||
DROP TABLE osint.cases;
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE osint.assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE,
|
||||
original_name TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
byte_size BIGINT NOT NULL CHECK (byte_size >= 0),
|
||||
content BYTEA NOT NULL,
|
||||
checksum_sha256 TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX assets_level_idx ON osint.assets(level_id, created_at);
|
||||
|
||||
ALTER TABLE osint.widgets
|
||||
ADD COLUMN asset_id TEXT REFERENCES osint.assets(id) ON DELETE SET NULL;
|
||||
|
||||
COMMENT ON TABLE osint.assets IS 'Uploaded source files used by database-backed document widgets';
|
||||
@@ -0,0 +1,56 @@
|
||||
-- Widget kinds are intentionally data-driven. Removing enum-like checks means
|
||||
-- future widget types do not require a table rewrite merely to exist.
|
||||
ALTER TABLE osint.widgets DROP CONSTRAINT IF EXISTS widgets_widget_type_check;
|
||||
ALTER TABLE osint.playthrough_widgets DROP CONSTRAINT IF EXISTS playthrough_widgets_widget_type_check;
|
||||
|
||||
ALTER TABLE osint.widgets ADD COLUMN published_at TIMESTAMPTZ;
|
||||
|
||||
UPDATE osint.widgets
|
||||
SET published_at = event_date::timestamp AT TIME ZONE 'UTC'
|
||||
WHERE widget_type = 'document' AND event_date IS NOT NULL;
|
||||
|
||||
CREATE TABLE osint.widget_relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
level_id TEXT NOT NULL REFERENCES osint.levels(id) ON DELETE CASCADE,
|
||||
from_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE,
|
||||
to_widget_id TEXT NOT NULL REFERENCES osint.widgets(id) ON DELETE CASCADE,
|
||||
relation_type TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (from_widget_id, to_widget_id, relation_type)
|
||||
);
|
||||
|
||||
CREATE INDEX widget_relations_from_idx ON osint.widget_relations(level_id, from_widget_id, relation_type);
|
||||
CREATE INDEX widget_relations_to_idx ON osint.widget_relations(level_id, to_widget_id, relation_type);
|
||||
|
||||
CREATE TABLE osint.playthrough_widget_relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE,
|
||||
from_widget_id TEXT NOT NULL,
|
||||
to_widget_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
UNIQUE (playthrough_id, from_widget_id, to_widget_id, relation_type)
|
||||
);
|
||||
|
||||
UPDATE osint.widgets SET widget_type = 'folder' WHERE widget_type = 'evidence';
|
||||
UPDATE osint.playthrough_widgets SET widget_type = 'folder' WHERE widget_type = 'evidence';
|
||||
|
||||
INSERT INTO osint.widget_relations (id, level_id, from_widget_id, to_widget_id, relation_type)
|
||||
SELECT 'contains:' || id || ':' || source_widget_id, level_id, id, source_widget_id, 'contains'
|
||||
FROM osint.widgets
|
||||
WHERE widget_type = 'folder' AND source_widget_id IS NOT NULL
|
||||
ON CONFLICT (from_widget_id, to_widget_id, relation_type) DO NOTHING;
|
||||
|
||||
INSERT INTO osint.playthrough_widget_relations
|
||||
(id, playthrough_id, from_widget_id, to_widget_id, relation_type)
|
||||
SELECT 'contains:' || playthrough_id || ':' || id || ':' || source_widget_id,
|
||||
playthrough_id, id, source_widget_id, 'contains'
|
||||
FROM osint.playthrough_widgets
|
||||
WHERE widget_type = 'folder' AND source_widget_id IS NOT NULL
|
||||
ON CONFLICT (playthrough_id, from_widget_id, to_widget_id, relation_type) DO NOTHING;
|
||||
|
||||
COMMENT ON COLUMN osint.widgets.published_at IS 'Publication/creation time of a source document; chronology belongs to documents, not folders';
|
||||
COMMENT ON TABLE osint.widget_relations IS 'Generic authored widget graph; contains is the first relation type';
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Runtime state is generic JSON so future widget and relation renderers can
|
||||
-- persist interaction-specific state without adding a column per widget kind.
|
||||
ALTER TABLE osint.playthrough_widget_state
|
||||
ADD COLUMN config JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
CREATE TABLE osint.playthrough_widget_relation_state (
|
||||
playthrough_id TEXT NOT NULL REFERENCES osint.playthroughs(id) ON DELETE CASCADE,
|
||||
relation_id TEXT NOT NULL REFERENCES osint.widget_relations(id) ON DELETE CASCADE,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (playthrough_id, relation_id)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN osint.playthrough_widget_state.config IS 'Player-owned runtime and metadata overrides for any authored widget type';
|
||||
COMMENT ON TABLE osint.playthrough_widget_relation_state IS 'Player-owned layout/state overrides for authored widget relations';
|
||||
Reference in New Issue
Block a user