mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 05:01:08 +02:00
- Integrate worldream (text-first world-building platform) into games/ - Configure as @worldream/web workspace package - Remove standalone git repo, now part of monorepo 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
396 lines
No EOL
11 KiB
PL/PgSQL
396 lines
No EOL
11 KiB
PL/PgSQL
-- Migration: Add Custom Fields System
|
|
-- Description: Adds custom schema and data fields to content_nodes for flexible user-defined mechanics
|
|
|
|
-- Add custom fields columns to content_nodes
|
|
ALTER TABLE content_nodes
|
|
ADD COLUMN IF NOT EXISTS custom_schema JSONB DEFAULT NULL,
|
|
ADD COLUMN IF NOT EXISTS custom_data JSONB DEFAULT NULL,
|
|
ADD COLUMN IF NOT EXISTS schema_version INTEGER DEFAULT 1;
|
|
|
|
-- Add indexes for custom fields queries
|
|
CREATE INDEX IF NOT EXISTS idx_content_nodes_custom_schema
|
|
ON content_nodes USING GIN (custom_schema);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_content_nodes_custom_data
|
|
ON content_nodes USING GIN (custom_data);
|
|
|
|
-- Add partial index for nodes with custom fields
|
|
CREATE INDEX IF NOT EXISTS idx_content_nodes_with_custom_fields
|
|
ON content_nodes (id)
|
|
WHERE custom_schema IS NOT NULL;
|
|
|
|
-- Create custom field templates table
|
|
CREATE TABLE IF NOT EXISTS custom_field_templates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
category TEXT DEFAULT 'community' CHECK (category IN ('official', 'community', 'personal')),
|
|
tags TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
applicable_to TEXT[] DEFAULT ARRAY[]::TEXT[], -- node kinds this template applies to
|
|
fields JSONB NOT NULL, -- array of field definitions
|
|
example_data JSONB,
|
|
author_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
world_slug TEXT,
|
|
version TEXT DEFAULT '1.0.0',
|
|
dependencies TEXT[] DEFAULT ARRAY[]::TEXT[], -- other template slugs
|
|
usage_count INTEGER DEFAULT 0,
|
|
is_public BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Add indexes for templates
|
|
CREATE INDEX IF NOT EXISTS idx_custom_field_templates_slug
|
|
ON custom_field_templates(slug);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_custom_field_templates_category
|
|
ON custom_field_templates(category);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_custom_field_templates_public
|
|
ON custom_field_templates(is_public)
|
|
WHERE is_public = true;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_custom_field_templates_author
|
|
ON custom_field_templates(author_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_custom_field_templates_world
|
|
ON custom_field_templates(world_slug);
|
|
|
|
-- Add RLS policies for custom_field_templates
|
|
ALTER TABLE custom_field_templates ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Policy: Anyone can view public templates
|
|
CREATE POLICY "Public templates are viewable by all" ON custom_field_templates
|
|
FOR SELECT
|
|
USING (is_public = true);
|
|
|
|
-- Policy: Users can view their own templates
|
|
CREATE POLICY "Users can view own templates" ON custom_field_templates
|
|
FOR SELECT
|
|
USING (author_id = auth.uid());
|
|
|
|
-- Policy: Users can view templates from their worlds
|
|
CREATE POLICY "Users can view world templates" ON custom_field_templates
|
|
FOR SELECT
|
|
USING (
|
|
world_slug IN (
|
|
SELECT slug FROM content_nodes
|
|
WHERE kind = 'world'
|
|
AND owner_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Policy: Users can create templates
|
|
CREATE POLICY "Users can create templates" ON custom_field_templates
|
|
FOR INSERT
|
|
WITH CHECK (author_id = auth.uid());
|
|
|
|
-- Policy: Users can update their own templates
|
|
CREATE POLICY "Users can update own templates" ON custom_field_templates
|
|
FOR UPDATE
|
|
USING (author_id = auth.uid());
|
|
|
|
-- Policy: Users can delete their own templates
|
|
CREATE POLICY "Users can delete own templates" ON custom_field_templates
|
|
FOR DELETE
|
|
USING (author_id = auth.uid());
|
|
|
|
-- Function to validate custom schema
|
|
CREATE OR REPLACE FUNCTION validate_custom_schema(
|
|
p_schema JSONB
|
|
)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_field JSONB;
|
|
v_field_type TEXT;
|
|
v_valid_types TEXT[] := ARRAY['text', 'number', 'range', 'select', 'multiselect', 'boolean', 'date', 'formula', 'reference', 'list', 'json'];
|
|
BEGIN
|
|
-- Check if schema has required structure
|
|
IF p_schema IS NULL OR NOT p_schema ? 'fields' THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
|
|
-- Validate each field
|
|
FOR v_field IN SELECT * FROM jsonb_array_elements(p_schema->'fields')
|
|
LOOP
|
|
-- Check required field properties
|
|
IF NOT (v_field ? 'id' AND v_field ? 'key' AND v_field ? 'label' AND v_field ? 'type') THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
|
|
-- Validate field type
|
|
v_field_type := v_field->>'type';
|
|
IF NOT (v_field_type = ANY(v_valid_types)) THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
|
|
-- Validate type-specific config
|
|
CASE v_field_type
|
|
WHEN 'number', 'range' THEN
|
|
-- Should have min/max if specified
|
|
IF v_field->'config' ? 'min' AND v_field->'config' ? 'max' THEN
|
|
IF (v_field->'config'->>'min')::NUMERIC > (v_field->'config'->>'max')::NUMERIC THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
END IF;
|
|
WHEN 'select', 'multiselect' THEN
|
|
-- Should have choices
|
|
IF NOT (v_field->'config' ? 'choices' AND jsonb_array_length(v_field->'config'->'choices') > 0) THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
WHEN 'reference' THEN
|
|
-- Should have reference_type
|
|
IF NOT (v_field->'config' ? 'reference_type') THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
ELSE
|
|
-- Other types don't need special validation yet
|
|
NULL;
|
|
END CASE;
|
|
END LOOP;
|
|
|
|
RETURN TRUE;
|
|
END;
|
|
$$;
|
|
|
|
-- Function to calculate formula fields
|
|
CREATE OR REPLACE FUNCTION calculate_formula_fields(
|
|
p_schema JSONB,
|
|
p_data JSONB
|
|
)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_field JSONB;
|
|
v_formula TEXT;
|
|
v_result JSONB;
|
|
BEGIN
|
|
v_result := p_data;
|
|
|
|
-- Process each formula field
|
|
FOR v_field IN
|
|
SELECT * FROM jsonb_array_elements(p_schema->'fields')
|
|
WHERE value->>'type' = 'formula'
|
|
LOOP
|
|
v_formula := v_field->'config'->>'formula';
|
|
|
|
-- For now, store the formula as-is
|
|
-- In production, we'd evaluate it here
|
|
v_result := jsonb_set(
|
|
v_result,
|
|
ARRAY[v_field->>'key'],
|
|
to_jsonb(v_formula)
|
|
);
|
|
END LOOP;
|
|
|
|
RETURN v_result;
|
|
END;
|
|
$$;
|
|
|
|
-- Function to apply template to node
|
|
CREATE OR REPLACE FUNCTION apply_field_template(
|
|
p_node_id UUID,
|
|
p_template_id UUID,
|
|
p_merge BOOLEAN DEFAULT false
|
|
)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_template custom_field_templates;
|
|
v_current_schema JSONB;
|
|
v_new_schema JSONB;
|
|
BEGIN
|
|
-- Get template
|
|
SELECT * INTO v_template
|
|
FROM custom_field_templates
|
|
WHERE id = p_template_id;
|
|
|
|
IF v_template IS NULL THEN
|
|
RAISE EXCEPTION 'Template not found';
|
|
END IF;
|
|
|
|
-- Get current schema if merging
|
|
IF p_merge THEN
|
|
SELECT custom_schema INTO v_current_schema
|
|
FROM content_nodes
|
|
WHERE id = p_node_id;
|
|
|
|
-- Merge fields arrays
|
|
v_new_schema := jsonb_build_object(
|
|
'version', COALESCE(v_current_schema->>'version', '1')::INT + 1,
|
|
'fields', COALESCE(v_current_schema->'fields', '[]'::JSONB) || v_template.fields,
|
|
'template_id', p_template_id::TEXT,
|
|
'template_version', v_template.version
|
|
);
|
|
ELSE
|
|
-- Replace with template
|
|
v_new_schema := jsonb_build_object(
|
|
'version', 1,
|
|
'fields', v_template.fields,
|
|
'template_id', p_template_id::TEXT,
|
|
'template_version', v_template.version
|
|
);
|
|
END IF;
|
|
|
|
-- Update node
|
|
UPDATE content_nodes
|
|
SET
|
|
custom_schema = v_new_schema,
|
|
custom_data = CASE
|
|
WHEN p_merge THEN COALESCE(custom_data, '{}'::JSONB)
|
|
ELSE v_template.example_data
|
|
END,
|
|
schema_version = (v_new_schema->>'version')::INT,
|
|
updated_at = NOW()
|
|
WHERE id = p_node_id;
|
|
|
|
-- Increment usage count
|
|
UPDATE custom_field_templates
|
|
SET usage_count = usage_count + 1
|
|
WHERE id = p_template_id;
|
|
|
|
RETURN TRUE;
|
|
END;
|
|
$$;
|
|
|
|
-- Insert some official starter templates
|
|
INSERT INTO custom_field_templates (
|
|
slug, name, description, category, tags, applicable_to, fields, example_data, is_public
|
|
) VALUES
|
|
(
|
|
'basic-stats',
|
|
'Basic Character Stats',
|
|
'Standard RPG character statistics',
|
|
'official',
|
|
ARRAY['rpg', 'stats', 'character'],
|
|
ARRAY['character'],
|
|
'[
|
|
{
|
|
"id": "str",
|
|
"key": "strength",
|
|
"label": "Stärke",
|
|
"type": "number",
|
|
"category": "attributes",
|
|
"description": "Physische Kraft",
|
|
"config": {"min": 1, "max": 20, "default": 10}
|
|
},
|
|
{
|
|
"id": "dex",
|
|
"key": "dexterity",
|
|
"label": "Geschicklichkeit",
|
|
"type": "number",
|
|
"category": "attributes",
|
|
"description": "Beweglichkeit und Reflexe",
|
|
"config": {"min": 1, "max": 20, "default": 10}
|
|
},
|
|
{
|
|
"id": "int",
|
|
"key": "intelligence",
|
|
"label": "Intelligenz",
|
|
"type": "number",
|
|
"category": "attributes",
|
|
"description": "Verstand und Wissen",
|
|
"config": {"min": 1, "max": 20, "default": 10}
|
|
},
|
|
{
|
|
"id": "hp",
|
|
"key": "health_points",
|
|
"label": "Lebenspunkte",
|
|
"type": "range",
|
|
"category": "resources",
|
|
"description": "Aktuelle/Maximale Lebenspunkte",
|
|
"config": {"min": 0, "max": 100, "default": 100}
|
|
}
|
|
]'::JSONB,
|
|
'{"strength": 10, "dexterity": 10, "intelligence": 10, "health_points": 100}'::JSONB,
|
|
true
|
|
),
|
|
(
|
|
'inventory-basic',
|
|
'Basic Inventory',
|
|
'Simple inventory management fields',
|
|
'official',
|
|
ARRAY['inventory', 'items', 'general'],
|
|
ARRAY['character', 'object', 'place'],
|
|
'[
|
|
{
|
|
"id": "inv",
|
|
"key": "inventory",
|
|
"label": "Inventar",
|
|
"type": "list",
|
|
"category": "items",
|
|
"description": "Liste von Gegenständen",
|
|
"config": {"item_type": "text"}
|
|
},
|
|
{
|
|
"id": "weight",
|
|
"key": "carry_weight",
|
|
"label": "Traglast",
|
|
"type": "number",
|
|
"category": "items",
|
|
"description": "Aktuelles Gewicht in kg",
|
|
"config": {"min": 0, "max": 1000, "default": 0, "unit": "kg"}
|
|
},
|
|
{
|
|
"id": "gold",
|
|
"key": "gold",
|
|
"label": "Gold",
|
|
"type": "number",
|
|
"category": "resources",
|
|
"description": "Verfügbares Gold",
|
|
"config": {"min": 0, "default": 0}
|
|
}
|
|
]'::JSONB,
|
|
'{"inventory": [], "carry_weight": 0, "gold": 100}'::JSONB,
|
|
true
|
|
),
|
|
(
|
|
'relationships',
|
|
'Relationship Tracker',
|
|
'Track relationships between characters',
|
|
'official',
|
|
ARRAY['social', 'relationships', 'character'],
|
|
ARRAY['character'],
|
|
'[
|
|
{
|
|
"id": "rep",
|
|
"key": "reputation",
|
|
"label": "Reputation",
|
|
"type": "range",
|
|
"category": "social",
|
|
"description": "Allgemeiner Ruf",
|
|
"config": {"min": -100, "max": 100, "default": 0}
|
|
},
|
|
{
|
|
"id": "allies",
|
|
"key": "allies",
|
|
"label": "Verbündete",
|
|
"type": "list",
|
|
"category": "relationships",
|
|
"description": "Liste von Verbündeten",
|
|
"config": {"item_type": "reference", "reference_type": "character"}
|
|
},
|
|
{
|
|
"id": "enemies",
|
|
"key": "enemies",
|
|
"label": "Feinde",
|
|
"type": "list",
|
|
"category": "relationships",
|
|
"description": "Liste von Feinden",
|
|
"config": {"item_type": "reference", "reference_type": "character"}
|
|
}
|
|
]'::JSONB,
|
|
'{"reputation": 0, "allies": [], "enemies": []}'::JSONB,
|
|
true
|
|
);
|
|
|
|
-- Add comment explaining the structure
|
|
COMMENT ON COLUMN content_nodes.custom_schema IS 'User-defined field schema for flexible mechanics - contains field definitions, types, and validation rules';
|
|
COMMENT ON COLUMN content_nodes.custom_data IS 'Actual values for the custom fields defined in custom_schema';
|
|
COMMENT ON COLUMN content_nodes.schema_version IS 'Version number for schema migrations and compatibility';
|
|
COMMENT ON TABLE custom_field_templates IS 'Reusable templates for custom field schemas that can be shared and applied to nodes'; |