feat(deploy): update deployment workflow and migration paths
Some checks failed
Deploy to production / deploy (push) Has been cancelled

- Modified the deployment workflow to include new migration paths for the backend, ensuring that migrations are correctly referenced in the deployment process.
- Updated the `db:migrate` script in package.json to point to the `migrations-active` directory, enhancing clarity and organization of migration files.
- Adjusted the deployment conditions to account for changes in migration file locations, improving the accuracy of change detection during deployments.
- Removed obsolete migration files to streamline the migration process and prevent confusion.
This commit is contained in:
Torsten Schulz (local)
2026-04-17 13:39:38 +02:00
parent 70c381114b
commit 5070785a50
54 changed files with 106 additions and 12 deletions

View File

@@ -0,0 +1,45 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const table = { tableName: 'contact_message', schema: 'service' };
const columns = await queryInterface.describeTable(table);
if (!columns.answer) {
await queryInterface.addColumn(table, 'answer', {
type: Sequelize.TEXT,
allowNull: true
});
}
if (!columns.answered_at) {
await queryInterface.addColumn(table, 'answered_at', {
type: Sequelize.DATE,
allowNull: true
});
}
if (!columns.is_answered) {
await queryInterface.addColumn(table, 'is_answered', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
}
},
down: async (queryInterface, Sequelize) => {
const table = { tableName: 'contact_message', schema: 'service' };
const columns = await queryInterface.describeTable(table);
if (columns.answer) {
await queryInterface.removeColumn(table, 'answer');
}
if (columns.answered_at) {
await queryInterface.removeColumn(table, 'answered_at');
}
if (columns.is_answered) {
await queryInterface.removeColumn(table, 'is_answered');
}
}
};

View File

@@ -0,0 +1,29 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn(
{
tableName: 'falukant_user',
schema: 'falukant_data'
},
'last_nobility_advance_at',
{
type: Sequelize.DATE,
allowNull: true
}
);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn(
{
tableName: 'falukant_user',
schema: 'falukant_data'
},
'last_nobility_advance_at'
);
}
};

View File

@@ -0,0 +1,135 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// 1) Add character_name column to notification table
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_log.notification
ADD COLUMN IF NOT EXISTS character_name text;
`);
// 1b) Add character_id column so triggers and application can set a reference
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_log.notification
ADD COLUMN IF NOT EXISTS character_id integer;
`);
// Create an index on character_id to speed lookups (if not exists)
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
) THEN
CREATE INDEX idx_notification_character_id ON falukant_log.notification (character_id);
END IF;
END$$;
`);
// 2) Create helper function to populate character_name from character_id or user_id
// - Resolve name via character_id if present
// - Fallback to a character for the same user_id when character_id is NULL
// - Only set NEW.character_name when the column exists and is NULL
await queryInterface.sequelize.query(`
CREATE OR REPLACE FUNCTION falukant_log.populate_notification_character_name()
RETURNS TRIGGER AS $function$
DECLARE
v_first_name TEXT;
v_last_name TEXT;
v_char_id INTEGER;
v_column_exists BOOLEAN;
BEGIN
-- check if target column exists in the notification table
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_log' AND table_name = 'notification' AND column_name = 'character_name'
) INTO v_column_exists;
IF NOT v_column_exists THEN
-- Nothing to do when target column absent
RETURN NEW;
END IF;
-- only populate when column is NULL
IF NEW.character_name IS NOT NULL THEN
RETURN NEW;
END IF;
-- prefer explicit character_id
v_char_id := NEW.character_id;
-- when character_id is null, try to find a character for the user_id
IF v_char_id IS NULL AND NEW.user_id IS NOT NULL THEN
-- choose a representative character: the one with highest id for this user (change if different policy required)
SELECT id INTO v_char_id
FROM falukant_data.character
WHERE user_id = NEW.user_id
ORDER BY id DESC
LIMIT 1;
END IF;
IF v_char_id IS NOT NULL THEN
SELECT pf.name, pl.name
INTO v_first_name, v_last_name
FROM falukant_data.character c
LEFT JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
LEFT JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
WHERE c.id = v_char_id;
IF v_first_name IS NOT NULL OR v_last_name IS NOT NULL THEN
NEW.character_name := COALESCE(v_first_name, '') || CASE WHEN v_first_name IS NOT NULL AND v_last_name IS NOT NULL THEN ' ' ELSE '' END || COALESCE(v_last_name, '');
ELSE
NEW.character_name := ('#' || v_char_id::text);
END IF;
ELSE
-- last resort fallback: use user_id as identifier if present
IF NEW.user_id IS NOT NULL THEN
NEW.character_name := ('#u' || NEW.user_id::text);
END IF;
END IF;
RETURN NEW;
END;
$function$ LANGUAGE plpgsql;
`);
// 3) Create trigger that runs before insert to populate the column
await queryInterface.sequelize.query(`
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
CREATE TRIGGER trg_populate_notification_character_name
BEFORE INSERT ON falukant_log.notification
FOR EACH ROW
EXECUTE FUNCTION falukant_log.populate_notification_character_name();
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
DROP TRIGGER IF EXISTS trg_populate_notification_character_name ON falukant_log.notification;
`);
await queryInterface.sequelize.query(`
DROP FUNCTION IF EXISTS falukant_log.populate_notification_character_name();
`);
await queryInterface.sequelize.query(`
-- drop index if exists
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'i' AND c.relname = 'idx_notification_character_id' AND n.nspname = 'falukant_log'
) THEN
EXECUTE 'DROP INDEX falukant_log.idx_notification_character_id';
END IF;
END$$;
`);
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_log.notification
DROP COLUMN IF EXISTS character_name;
`);
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_log.notification
DROP COLUMN IF EXISTS character_id;
`);
}
};

View File

@@ -0,0 +1,68 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// Add nullable weather_type_id column
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.production
ADD COLUMN IF NOT EXISTS weather_type_id integer;
`);
// Add foreign key constraint if not exists
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.constraint_schema = tc.constraint_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.constraint_schema = 'falukant_data'
AND tc.table_name = 'production'
AND kcu.column_name = 'weather_type_id'
) THEN
ALTER TABLE falukant_data.production
ADD CONSTRAINT fk_production_weather_type
FOREIGN KEY (weather_type_id) REFERENCES falukant_type.weather(id);
END IF;
END$$;
`);
// create index to speed lookups
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
) THEN
CREATE INDEX idx_production_weather_type_id ON falukant_data.production (weather_type_id);
END IF;
END$$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.production
DROP CONSTRAINT IF EXISTS fk_production_weather_type;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'i' AND c.relname = 'idx_production_weather_type_id' AND n.nspname = 'falukant_data'
) THEN
EXECUTE 'DROP INDEX falukant_data.idx_production_weather_type_id';
END IF;
END$$;
`);
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.production
DROP COLUMN IF EXISTS weather_type_id;
`);
}
};

View File

@@ -0,0 +1,17 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.stock
ADD COLUMN IF NOT EXISTS product_quality integer;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.stock
DROP COLUMN IF EXISTS product_quality;
`);
}
};

View File

@@ -0,0 +1,79 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// falukant_data.character.reputation (integer, default random 20..80)
// Wichtig: Schema explizit angeben
// Vorgehen:
// - Spalte anlegen (falls noch nicht vorhanden)
// - bestehende Zeilen initialisieren (random 20..80)
// - DEFAULT setzen (random 20..80)
// - NOT NULL + CHECK 0..100 erzwingen
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'character'
AND column_name = 'reputation'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN reputation integer;
END IF;
END$$;
`);
// Backfill: nur NULLs initialisieren (damit bestehende Werte nicht überschrieben werden)
await queryInterface.sequelize.query(`
UPDATE falukant_data."character"
SET reputation = (floor(random()*61)+20)::int
WHERE reputation IS NULL;
`);
// DEFAULT + NOT NULL (nach Backfill)
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
ALTER COLUMN reputation SET DEFAULT (floor(random()*61)+20)::int;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
ALTER COLUMN reputation SET NOT NULL;
`);
// Enforce 0..100 at DB level (percent)
// (IF NOT EXISTS pattern, because deployments can be re-run)
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE c.conname = 'character_reputation_0_100_chk'
AND n.nspname = 'falukant_data'
AND t.relname = 'character'
) THEN
ALTER TABLE falukant_data."character"
ADD CONSTRAINT character_reputation_0_100_chk
CHECK (reputation >= 0 AND reputation <= 100);
END IF;
END$$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
DROP CONSTRAINT IF EXISTS character_reputation_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
DROP COLUMN IF EXISTS reputation;
`);
},
};

View File

@@ -0,0 +1,47 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Typ-Tabelle (konfigurierbar ohne Code): falukant_type.reputation_action
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_type.reputation_action (
id serial PRIMARY KEY,
tr text NOT NULL UNIQUE,
cost integer NOT NULL CHECK (cost >= 0),
base_gain integer NOT NULL CHECK (base_gain >= 0),
decay_factor double precision NOT NULL CHECK (decay_factor > 0 AND decay_factor <= 1),
min_gain integer NOT NULL DEFAULT 0 CHECK (min_gain >= 0),
decay_window_days integer NOT NULL DEFAULT 7 CHECK (decay_window_days >= 1 AND decay_window_days <= 365)
);
`);
// Log-Tabelle: falukant_log.reputation_action
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_log.reputation_action (
id serial PRIMARY KEY,
falukant_user_id integer NOT NULL,
action_type_id integer NOT NULL,
cost integer NOT NULL CHECK (cost >= 0),
base_gain integer NOT NULL CHECK (base_gain >= 0),
gain integer NOT NULL CHECK (gain >= 0),
times_used_before integer NOT NULL CHECK (times_used_before >= 0),
action_timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS reputation_action_log_user_type_idx
ON falukant_log.reputation_action (falukant_user_id, action_type_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS reputation_action_log_ts_idx
ON falukant_log.reputation_action (action_timestamp);
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_log.reputation_action;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_type.reputation_action;`);
},
};

View File

@@ -0,0 +1,46 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Für bereits existierende Installationen: Spalte sicherstellen + Backfill
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ADD COLUMN IF NOT EXISTS decay_window_days integer;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_type.reputation_action
SET decay_window_days = 7
WHERE decay_window_days IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ALTER COLUMN decay_window_days SET DEFAULT 7;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ALTER COLUMN decay_window_days SET NOT NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ADD CONSTRAINT reputation_action_decay_window_days_chk
CHECK (decay_window_days >= 1 AND decay_window_days <= 365);
`);
},
async down(queryInterface, Sequelize) {
// optional: wieder entfernen
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
DROP COLUMN IF EXISTS decay_window_days;
`);
},
};

View File

@@ -0,0 +1,50 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Idempotentes Seed: legt Ruf-Aktionen an bzw. aktualisiert sie anhand "tr"
await queryInterface.sequelize.query(`
INSERT INTO falukant_type.reputation_action
(tr, cost, base_gain, decay_factor, min_gain, decay_window_days)
VALUES
('soup_kitchen', 500, 2, 0.85, 0, 7),
('library_donation', 5000, 4, 0.88, 0, 7),
('well_build', 8000, 4, 0.87, 0, 7),
('scholarships', 10000, 5, 0.87, 0, 7),
('church_hospice', 12000, 5, 0.87, 0, 7),
('school_funding', 15000, 6, 0.88, 0, 7),
('orphanage_build', 20000, 7, 0.90, 0, 7),
('bridge_build', 25000, 7, 0.90, 0, 7),
('hospital_donation', 30000, 8, 0.90, 0, 7),
('patronage', 40000, 9, 0.91, 0, 7),
('statue_build', 50000, 10, 0.92, 0, 7)
ON CONFLICT (tr) DO UPDATE SET
cost = EXCLUDED.cost,
base_gain = EXCLUDED.base_gain,
decay_factor = EXCLUDED.decay_factor,
min_gain = EXCLUDED.min_gain,
decay_window_days = EXCLUDED.decay_window_days;
`);
},
async down(queryInterface, Sequelize) {
// Entfernt nur die gesetzten Seeds (tr-basiert)
await queryInterface.sequelize.query(`
DELETE FROM falukant_type.reputation_action
WHERE tr IN (
'soup_kitchen',
'library_donation',
'well_build',
'scholarships',
'church_hospice',
'school_funding',
'orphanage_build',
'bridge_build',
'hospital_donation',
'patronage',
'statue_build'
);
`);
},
};

View File

@@ -0,0 +1,60 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Ensure column exists
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ADD COLUMN IF NOT EXISTS condition integer;
`);
// Backfill nulls (legacy data)
await queryInterface.sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = 100
WHERE condition IS NULL;
`);
// Clamp out-of-range values defensively
await queryInterface.sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = GREATEST(0, LEAST(100, condition))
WHERE condition < 0 OR condition > 100;
`);
// Default + NOT NULL
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ALTER COLUMN condition SET DEFAULT 100;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ALTER COLUMN condition SET NOT NULL;
`);
// Check constraint 0..100
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ADD CONSTRAINT vehicle_condition_0_100_chk
CHECK (condition >= 0 AND condition <= 100);
`);
},
async down(queryInterface, Sequelize) {
// Keep the column, but remove constraint/default to be reversible
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ALTER COLUMN condition DROP DEFAULT;
`);
// NOT NULL not reverted to avoid introducing NULLs on rollback; can be adjusted if needed
},
};

View File

@@ -0,0 +1,32 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
ADD COLUMN IF NOT EXISTS may_repair_vehicles boolean;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_data.director
SET may_repair_vehicles = true
WHERE may_repair_vehicles IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
ALTER COLUMN may_repair_vehicles SET DEFAULT true;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
ALTER COLUMN may_repair_vehicles SET NOT NULL;
`);
},
async down(queryInterface, Sequelize) {
// optional rollback: drop column
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
DROP COLUMN IF EXISTS may_repair_vehicles;
`);
},
};

View File

@@ -0,0 +1,61 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Sprache / Set, das geteilt werden kann
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_language (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
name TEXT NOT NULL,
share_code TEXT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_language_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_share_code_uniq UNIQUE (share_code)
);
`);
// Abos (Freunde)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_language_subscription (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_language_subscription_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_subscription_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_subscription_uniq UNIQUE (user_id, language_id)
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_language_owner_idx
ON community.vocab_language(owner_user_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_language_subscription_user_idx
ON community.vocab_language_subscription(user_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_language_subscription_language_idx
ON community.vocab_language_subscription(language_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language_subscription;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language;`);
}
};

View File

@@ -0,0 +1,106 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Kapitel innerhalb einer Sprache
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_chapter (
id SERIAL PRIMARY KEY,
language_id INTEGER NOT NULL,
title TEXT NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_chapter_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chapter_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chapter_language_idx
ON community.vocab_chapter(language_id);
`);
// Lexeme/Wörter (wir deduplizieren pro Sprache über normalized)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_lexeme (
id SERIAL PRIMARY KEY,
language_id INTEGER NOT NULL,
text TEXT NOT NULL,
normalized TEXT NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_lexeme_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_lexeme_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_lexeme_unique_per_language UNIQUE (language_id, normalized)
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_lexeme_language_idx
ON community.vocab_lexeme(language_id);
`);
// n:m Zuordnung pro Kapitel: Lernwort ↔ Referenzwort (Mehrdeutigkeiten möglich)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_chapter_lexeme (
id SERIAL PRIMARY KEY,
chapter_id INTEGER NOT NULL,
learning_lexeme_id INTEGER NOT NULL,
reference_lexeme_id INTEGER NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_chlex_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_learning_fk
FOREIGN KEY (learning_lexeme_id)
REFERENCES community.vocab_lexeme(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_reference_fk
FOREIGN KEY (reference_lexeme_id)
REFERENCES community.vocab_lexeme(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_unique UNIQUE (chapter_id, learning_lexeme_id, reference_lexeme_id)
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chlex_chapter_idx
ON community.vocab_chapter_lexeme(chapter_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chlex_learning_idx
ON community.vocab_chapter_lexeme(learning_lexeme_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
ON community.vocab_chapter_lexeme(reference_lexeme_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter_lexeme;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_lexeme;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter;`);
}
};

View File

@@ -0,0 +1,17 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.region
ADD COLUMN IF NOT EXISTS tax_percent numeric NOT NULL DEFAULT 7;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_data.region
DROP COLUMN IF EXISTS tax_percent;
`);
}
};

View File

@@ -0,0 +1,50 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// 1) add backup column for original sell_cost (idempotent)
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_type.product
ADD COLUMN IF NOT EXISTS original_sell_cost numeric;
`);
// 2) if original_sell_cost is not set, copy current sell_cost into it
await queryInterface.sequelize.query(`
UPDATE falukant_type.product
SET original_sell_cost = sell_cost
WHERE original_sell_cost IS NULL;
`);
// 3) compute max cumulative tax across regions and increase sell_cost accordingly
// We use the maximum cumulative tax (worst-case) so sellers are neutral across regions.
// Formula: neutral_sell = CEIL(original_sell_cost * (1 / (1 - max_total/100)))
await queryInterface.sequelize.query(`
WITH RECURSIVE ancestors AS (
SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
UNION ALL
SELECT a.start_id, r.id, r.parent_id, r.tax_percent
FROM falukant_data.region r
JOIN ancestors a ON r.id = a.parent_id
), totals AS (
SELECT start_id, COALESCE(SUM(tax_percent), 0) AS total FROM ancestors GROUP BY start_id
), mm AS (
SELECT COALESCE(MAX(total),0) AS max_total FROM totals
)
UPDATE falukant_type.product
SET sell_cost = CEIL(original_sell_cost * (CASE WHEN (1 - mm.max_total/100) <= 0 THEN 1 ELSE (1 / (1 - mm.max_total/100)) END))
FROM mm
WHERE original_sell_cost IS NOT NULL;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_type.product
DROP COLUMN IF EXISTS sell_cost_min_neutral;
`);
await queryInterface.sequelize.query(`
ALTER TABLE IF EXISTS falukant_type.product
DROP COLUMN IF EXISTS sell_cost_max_neutral;
`);
}
};

View File

@@ -0,0 +1,132 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Kurs-Tabelle
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
language_id INTEGER NOT NULL,
difficulty_level INTEGER DEFAULT 1,
is_public BOOLEAN DEFAULT false,
share_code TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
);
`);
// Lektionen innerhalb eines Kurses
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
id SERIAL PRIMARY KEY,
course_id INTEGER NOT NULL,
chapter_id INTEGER NOT NULL,
lesson_number INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_lesson_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
);
`);
// Einschreibungen in Kurse
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_enrollment_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
);
`);
// Fortschritt pro User und Lektion
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
lesson_id INTEGER NOT NULL,
completed BOOLEAN DEFAULT false,
score INTEGER DEFAULT 0,
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_course_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
);
`);
// Indizes
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
ON community.vocab_course(owner_user_id);
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
ON community.vocab_course(language_id);
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
ON community.vocab_course(is_public);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
ON community.vocab_course_lesson(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
ON community.vocab_course_lesson(chapter_id);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
ON community.vocab_course_enrollment(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
ON community.vocab_course_enrollment(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
ON community.vocab_course_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
ON community.vocab_course_progress(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
ON community.vocab_course_progress(lesson_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_course_progress CASCADE;
DROP TABLE IF EXISTS community.vocab_course_enrollment CASCADE;
DROP TABLE IF EXISTS community.vocab_course_lesson CASCADE;
DROP TABLE IF EXISTS community.vocab_course CASCADE;
`);
}
};

View File

@@ -0,0 +1,101 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Grammatik-Übungstypen (z.B. "gap_fill", "multiple_choice", "sentence_building", "transformation")
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
);
`);
// Grammatik-Übungen (verknüpft mit Lektionen)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
id SERIAL PRIMARY KEY,
lesson_id INTEGER NOT NULL,
exercise_type_id INTEGER NOT NULL,
exercise_number INTEGER NOT NULL,
title TEXT NOT NULL,
instruction TEXT,
question_data JSONB NOT NULL,
answer_data JSONB NOT NULL,
explanation TEXT,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_grammar_exercise_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_type_fk
FOREIGN KEY (exercise_type_id)
REFERENCES community.vocab_grammar_exercise_type(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
);
`);
// Fortschritt für Grammatik-Übungen
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
exercise_id INTEGER NOT NULL,
attempts INTEGER DEFAULT 0,
correct_attempts INTEGER DEFAULT 0,
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
completed BOOLEAN DEFAULT false,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_grammar_exercise_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
FOREIGN KEY (exercise_id)
REFERENCES community.vocab_grammar_exercise(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
);
`);
// Indizes
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
ON community.vocab_grammar_exercise(lesson_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
ON community.vocab_grammar_exercise(exercise_type_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
ON community.vocab_grammar_exercise_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
ON community.vocab_grammar_exercise_progress(exercise_id);
`);
// Standard-Übungstypen einfügen
await queryInterface.sequelize.query(`
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('gap_fill', 'Lückentext-Übung'),
('multiple_choice', 'Multiple-Choice-Fragen'),
('sentence_building', 'Satzbau-Übung'),
('transformation', 'Satzumformung'),
('conjugation', 'Konjugations-Übung'),
('declension', 'Deklinations-Übung')
ON CONFLICT (name) DO NOTHING;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_grammar_exercise_progress CASCADE;
DROP TABLE IF EXISTS community.vocab_grammar_exercise CASCADE;
DROP TABLE IF EXISTS community.vocab_grammar_exercise_type CASCADE;
`);
}
};

View File

@@ -0,0 +1,47 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// chapter_id optional machen (nicht alle Lektionen brauchen ein Kapitel)
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ALTER COLUMN chapter_id DROP NOT NULL;
`);
// Kurs-Wochen/Module hinzufügen
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS week_number INTEGER,
ADD COLUMN IF NOT EXISTS day_number INTEGER,
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
ADD COLUMN IF NOT EXISTS audio_url TEXT,
ADD COLUMN IF NOT EXISTS cultural_notes TEXT;
`);
// Indizes für Wochen/Tage
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
ON community.vocab_course_lesson(course_id, week_number);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
ON community.vocab_course_lesson(lesson_type);
`);
// Kommentar hinzufügen für lesson_type
await queryInterface.sequelize.query(`
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
'Type: vocab, grammar, conversation, culture, review';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS week_number,
DROP COLUMN IF EXISTS day_number,
DROP COLUMN IF EXISTS lesson_type,
DROP COLUMN IF EXISTS audio_url,
DROP COLUMN IF EXISTS cultural_notes;
`);
}
};

View File

@@ -0,0 +1,33 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Lernziele für Lektionen
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
`);
// Kommentare hinzufügen
await queryInterface.sequelize.query(`
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
'Zielzeit in Minuten für diese Lektion';
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS target_minutes,
DROP COLUMN IF EXISTS target_score_percent,
DROP COLUMN IF EXISTS requires_review;
`);
}
};

View File

@@ -0,0 +1,30 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// Create index on (user_id, shown) to optimize markNotificationsShown queries
// This prevents deadlocks by allowing fast lookups and reducing lock contention
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'i'
AND c.relname = 'idx_notification_user_id_shown'
AND n.nspname = 'falukant_log'
) THEN
CREATE INDEX idx_notification_user_id_shown
ON falukant_log.notification (user_id, shown);
END IF;
END$$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
DROP INDEX IF EXISTS falukant_log.idx_notification_user_id_shown;
`);
}
};

View File

@@ -0,0 +1,122 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.relationship_state (
id serial PRIMARY KEY,
relationship_id integer NOT NULL UNIQUE,
marriage_satisfaction integer NOT NULL DEFAULT 55 CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100),
marriage_public_stability integer NOT NULL DEFAULT 55 CHECK (marriage_public_stability >= 0 AND marriage_public_stability <= 100),
lover_role text NULL CHECK (lover_role IN ('secret_affair', 'lover', 'mistress_or_favorite')),
affection integer NOT NULL DEFAULT 50 CHECK (affection >= 0 AND affection <= 100),
visibility integer NOT NULL DEFAULT 15 CHECK (visibility >= 0 AND visibility <= 100),
discretion integer NOT NULL DEFAULT 50 CHECK (discretion >= 0 AND discretion <= 100),
maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100),
status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2),
monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0),
months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0),
active boolean NOT NULL DEFAULT true,
acknowledged boolean NOT NULL DEFAULT false,
exclusive_flag boolean NOT NULL DEFAULT false,
last_monthly_processed_at timestamp with time zone NULL,
last_daily_processed_at timestamp with time zone NULL,
notes_json jsonb NULL,
flags_json jsonb NULL,
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT relationship_state_relationship_fk
FOREIGN KEY (relationship_id)
REFERENCES falukant_data.relationship(id)
ON DELETE CASCADE
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS relationship_state_active_idx
ON falukant_data.relationship_state (active);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS relationship_state_lover_role_idx
ON falukant_data.relationship_state (lover_role);
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
ADD COLUMN IF NOT EXISTS legitimacy text NOT NULL DEFAULT 'legitimate';
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
ADD COLUMN IF NOT EXISTS birth_context text NOT NULL DEFAULT 'marriage';
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'child_relation_legitimacy_chk'
) THEN
ALTER TABLE falukant_data.child_relation
ADD CONSTRAINT child_relation_legitimacy_chk
CHECK (legitimacy IN ('legitimate', 'acknowledged_bastard', 'hidden_bastard'));
END IF;
END
$$;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'child_relation_birth_context_chk'
) THEN
ALTER TABLE falukant_data.child_relation
ADD CONSTRAINT child_relation_birth_context_chk
CHECK (birth_context IN ('marriage', 'lover'));
END IF;
END
$$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
DROP CONSTRAINT IF EXISTS child_relation_birth_context_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
DROP CONSTRAINT IF EXISTS child_relation_legitimacy_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
DROP COLUMN IF EXISTS public_known;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
DROP COLUMN IF EXISTS birth_context;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.child_relation
DROP COLUMN IF EXISTS legitimacy;
`);
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS falukant_data.relationship_state;
`);
},
};

View File

@@ -0,0 +1,60 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
INSERT INTO falukant_data.relationship_state (
relationship_id,
marriage_satisfaction,
marriage_public_stability,
lover_role,
affection,
visibility,
discretion,
maintenance_level,
status_fit,
monthly_base_cost,
months_underfunded,
active,
acknowledged,
exclusive_flag,
created_at,
updated_at
)
SELECT
r.id,
55,
55,
CASE WHEN rt.tr = 'lover' THEN 'lover' ELSE NULL END,
50,
CASE WHEN rt.tr = 'lover' THEN 20 ELSE 15 END,
CASE WHEN rt.tr = 'lover' THEN 45 ELSE 50 END,
50,
0,
CASE WHEN rt.tr = 'lover' THEN 30 ELSE 0 END,
0,
true,
false,
false,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM falukant_data.relationship r
INNER JOIN falukant_type.relationship rt
ON rt.id = r.relationship_type_id
LEFT JOIN falukant_data.relationship_state rs
ON rs.relationship_id = r.id
WHERE rs.id IS NULL
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
DELETE FROM falukant_data.relationship_state rs
USING falukant_data.relationship r
INNER JOIN falukant_type.relationship rt
ON rt.id = r.relationship_type_id
WHERE rs.relationship_id = r.id
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
`);
},
};

View File

@@ -0,0 +1,53 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'servant_count',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
}
);
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'servant_quality',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 50
}
);
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'servant_pay_level',
{
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'normal'
}
);
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_order',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 55
}
);
},
async down(queryInterface) {
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'household_order');
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_pay_level');
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_quality');
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_count');
}
};

View File

@@ -0,0 +1,36 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_score',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 10
}
);
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_reasons_json',
{
type: Sequelize.JSONB,
allowNull: true
}
);
},
async down(queryInterface) {
await queryInterface.removeColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_reasons_json'
);
await queryInterface.removeColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_score'
);
}
};

View File

@@ -0,0 +1,83 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
await queryInterface.addColumn(table, 'status', {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'delinquent'
});
await queryInterface.addColumn(table, 'entered_at', {
type: Sequelize.DATE,
allowNull: true
});
await queryInterface.addColumn(table, 'released_at', {
type: Sequelize.DATE,
allowNull: true
});
await queryInterface.addColumn(table, 'debt_at_entry', {
type: Sequelize.DECIMAL(14, 2),
allowNull: true
});
await queryInterface.addColumn(table, 'remaining_debt', {
type: Sequelize.DECIMAL(14, 2),
allowNull: true
});
await queryInterface.addColumn(table, 'days_overdue', {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
});
await queryInterface.addColumn(table, 'reason', {
type: Sequelize.STRING,
allowNull: true
});
await queryInterface.addColumn(table, 'creditworthiness_penalty', {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
});
await queryInterface.addColumn(table, 'next_forced_action', {
type: Sequelize.STRING,
allowNull: true
});
await queryInterface.addColumn(table, 'assets_seized_json', {
type: Sequelize.JSONB,
allowNull: true
});
await queryInterface.addColumn(table, 'public_known', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
async down(queryInterface) {
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
await queryInterface.removeColumn(table, 'public_known');
await queryInterface.removeColumn(table, 'assets_seized_json');
await queryInterface.removeColumn(table, 'next_forced_action');
await queryInterface.removeColumn(table, 'creditworthiness_penalty');
await queryInterface.removeColumn(table, 'reason');
await queryInterface.removeColumn(table, 'days_overdue');
await queryInterface.removeColumn(table, 'remaining_debt');
await queryInterface.removeColumn(table, 'debt_at_entry');
await queryInterface.removeColumn(table, 'released_at');
await queryInterface.removeColumn(table, 'entered_at');
await queryInterface.removeColumn(table, 'status');
}
};

View File

@@ -0,0 +1,22 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.transport
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.underground
ALTER COLUMN victim_id DROP NOT NULL;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.transport
DROP COLUMN IF EXISTS guard_count;
`);
}
};

View File

@@ -0,0 +1,36 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS learning_goals JSONB,
ADD COLUMN IF NOT EXISTS core_patterns JSONB,
ADD COLUMN IF NOT EXISTS grammar_focus JSONB,
ADD COLUMN IF NOT EXISTS speaking_prompts JSONB,
ADD COLUMN IF NOT EXISTS practical_tasks JSONB;
`);
await queryInterface.sequelize.query(`
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('dialog_completion', 'Dialogergänzung'),
('situational_response', 'Situative Antwort'),
('pattern_drill', 'Muster-Drill'),
('reading_aloud', 'Lautlese-Übung'),
('speaking_from_memory', 'Freies Sprechen')
ON CONFLICT (name) DO NOTHING;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS practical_tasks,
DROP COLUMN IF EXISTS speaking_prompts,
DROP COLUMN IF EXISTS grammar_focus,
DROP COLUMN IF EXISTS core_patterns,
DROP COLUMN IF EXISTS learning_goals;
`);
}
};

View File

@@ -0,0 +1,18 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.user_param
ALTER COLUMN value TYPE TEXT;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.user_param
ALTER COLUMN value TYPE VARCHAR(255);
`);
}
};

View File

@@ -0,0 +1,31 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'community', tableName: 'folder' },
'is_adult_area',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
);
await queryInterface.addColumn(
{ schema: 'community', tableName: 'image' },
'is_adult_content',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
);
},
async down(queryInterface) {
await queryInterface.removeColumn({ schema: 'community', tableName: 'image' }, 'is_adult_content');
await queryInterface.removeColumn({ schema: 'community', tableName: 'folder' }, 'is_adult_area');
},
};

View File

@@ -0,0 +1,63 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
title: {
type: Sequelize.STRING,
allowNull: false,
},
description: {
type: Sequelize.TEXT,
allowNull: true,
},
original_file_name: {
type: Sequelize.STRING,
allowNull: false,
},
hash: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
mime_type: {
type: Sequelize.STRING,
allowNull: false,
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'user' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
}
);
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video' });
},
};

View File

@@ -0,0 +1,20 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'chat', tableName: 'room' },
'is_adult_only',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
);
},
async down(queryInterface) {
await queryInterface.removeColumn({ schema: 'chat', tableName: 'room' }, 'is_adult_only');
},
};

View File

@@ -0,0 +1,95 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'community', tableName: 'image' },
'is_moderated_hidden',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
}
).catch(() => {});
await queryInterface.addColumn(
{ schema: 'community', tableName: 'erotic_video' },
'is_moderated_hidden',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
}
).catch(() => {});
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_content_report' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
reporter_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: { model: { schema: 'community', tableName: 'user' }, key: 'id' },
onDelete: 'CASCADE'
},
target_type: {
type: Sequelize.STRING(20),
allowNull: false
},
target_id: {
type: Sequelize.INTEGER,
allowNull: false
},
reason: {
type: Sequelize.STRING(80),
allowNull: false
},
note: {
type: Sequelize.TEXT,
allowNull: true
},
status: {
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'open'
},
action_taken: {
type: Sequelize.STRING(40),
allowNull: true
},
handled_by: {
type: Sequelize.INTEGER,
allowNull: true,
references: { model: { schema: 'community', tableName: 'user' }, key: 'id' },
onDelete: 'SET NULL'
},
handled_at: {
type: Sequelize.DATE,
allowNull: true
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('NOW()')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('NOW()')
}
}
).catch(() => {});
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_content_report' }).catch(() => {});
await queryInterface.removeColumn({ schema: 'community', tableName: 'erotic_video' }, 'is_moderated_hidden').catch(() => {});
await queryInterface.removeColumn({ schema: 'community', tableName: 'image' }, 'is_moderated_hidden').catch(() => {});
}
};

View File

@@ -0,0 +1,89 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
visibility_type_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'type', tableName: 'image_visibility' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'user' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.sequelize.query(`
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
SELECT ev.id, iv.id
FROM community.erotic_video ev
CROSS JOIN type.image_visibility iv
WHERE iv.description = 'adults'
AND NOT EXISTS (
SELECT 1
FROM community.erotic_video_image_visibility eviv
WHERE eviv.erotic_video_id = ev.id
AND eviv.visibility_type_id = iv.id
)
`);
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
},
};

View File

@@ -0,0 +1,36 @@
"use strict";
/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_father_character_id INTEGER NULL
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;
END IF;
END$$;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at;
`);
},
};

View File

@@ -0,0 +1,44 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS didactic_mode TEXT,
ADD COLUMN IF NOT EXISTS phase_label TEXT,
ADD COLUMN IF NOT EXISTS block_number INTEGER,
ADD COLUMN IF NOT EXISTS difficulty_weight INTEGER,
ADD COLUMN IF NOT EXISTS new_unit_target INTEGER,
ADD COLUMN IF NOT EXISTS review_weight INTEGER,
ADD COLUMN IF NOT EXISTS is_intensive_review BOOLEAN NOT NULL DEFAULT FALSE;
COMMENT ON COLUMN community.vocab_course_lesson.didactic_mode IS
'Didaktischer Modus der Lektion, z.B. core_input, guided_dialogue, intensive_review oder checkpoint.';
COMMENT ON COLUMN community.vocab_course_lesson.phase_label IS
'Übergeordnete Lernphase, z.B. quickstart, daily_life oder stabilization.';
COMMENT ON COLUMN community.vocab_course_lesson.block_number IS
'Inhaltlicher Block für Konsolidierungs- und Wiederholungswellen.';
COMMENT ON COLUMN community.vocab_course_lesson.difficulty_weight IS
'Grobe relative Schwierigkeit der Lektion von leicht bis schwer.';
COMMENT ON COLUMN community.vocab_course_lesson.new_unit_target IS
'Empfohlene Zahl neuer Spracheinheiten in dieser Lektion.';
COMMENT ON COLUMN community.vocab_course_lesson.review_weight IS
'Wie stark Wiederholung in dieser Lektion dominieren soll, typischerweise 0 bis 100.';
COMMENT ON COLUMN community.vocab_course_lesson.is_intensive_review IS
'Markiert Lektionen, die als intensive Wiederholungsphase gedacht sind.';
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS is_intensive_review,
DROP COLUMN IF EXISTS review_weight,
DROP COLUMN IF EXISTS new_unit_target,
DROP COLUMN IF EXISTS difficulty_weight,
DROP COLUMN IF EXISTS block_number,
DROP COLUMN IF EXISTS phase_label,
DROP COLUMN IF EXISTS didactic_mode;
`);
}
};

View File

@@ -0,0 +1,20 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_progress
ADD COLUMN IF NOT EXISTS lesson_state JSONB NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN community.vocab_course_progress.lesson_state IS
'Persistierter UI- und Lernzustand pro Nutzer und Lektion fuer Resume im Sprachkurs.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_progress
DROP COLUMN IF EXISTS lesson_state;
`);
}
};

View File

@@ -0,0 +1,72 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) THEN
ALTER TABLE falukant_predefine.political_office_benefit
RENAME COLUMN political_office_id TO office_type_id;
ELSIF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) THEN
UPDATE falukant_predefine.political_office_benefit
SET office_type_id = COALESCE(office_type_id, political_office_id);
ALTER TABLE falukant_predefine.political_office_benefit
DROP COLUMN political_office_id;
END IF;
END $$;
`);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS last_political_daily_salary_on;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) THEN
ALTER TABLE falukant_predefine.political_office_benefit
RENAME COLUMN office_type_id TO political_office_id;
END IF;
END $$;
`);
}
};

View File

@@ -0,0 +1,30 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
INSERT INTO type.user_param_value (user_param_type_id, value, order_id)
SELECT upt.id, 'fr', COALESCE(
(SELECT MAX(v.order_id) FROM type.user_param_value v WHERE v.user_param_type_id = upt.id),
0
) + 1
FROM type.user_param upt
WHERE upt.description = 'language'
AND NOT EXISTS (
SELECT 1 FROM type.user_param_value x
WHERE x.user_param_type_id = upt.id AND x.value = 'fr'
);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DELETE FROM type.user_param_value v
USING type.user_param upt
WHERE v.user_param_type_id = upt.id
AND upt.description = 'language'
AND v.value = 'fr';
`);
},
};

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS certificate_productions_count_since TIMESTAMPTZ;
`);
await queryInterface.sequelize.query(`
COMMENT ON COLUMN falukant_data.falukant_user.certificate_productions_count_since IS
'Daemon/UI: Zählt nur falukant_log.production-Zeilen mit COALESCE(production_timestamp, production_date::timestamp) >= diesem Wert; bei Stufenänderung (Aufstieg/Bankrott/Erbfolge) auf NOW() (YpDaemon QUERY_UPDATE_FALUKANT_USER_CERTIFICATE). NULL = alle passenden Log-Zeilen bis zur ersten Stufenänderung nach Migration. Kein Löschen der Logs zum Reset.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS certificate_productions_count_since;
`);
}
};

View File

@@ -0,0 +1,61 @@
'use strict';
/** @param {import('sequelize').QueryInterface} queryInterface */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.political_benefit_last_tick (
id serial PRIMARY KEY,
character_id integer NOT NULL
REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
political_office_benefit_id integer NOT NULL
REFERENCES falukant_predefine.political_office_benefit(id) ON DELETE CASCADE,
last_tick_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
ticks_count integer NOT NULL DEFAULT 0,
CONSTRAINT political_benefit_last_tick_unique UNIQUE (character_id, political_office_benefit_id)
);
CREATE INDEX IF NOT EXISTS political_benefit_last_tick_character_idx
ON falukant_data.political_benefit_last_tick (character_id);
`);
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.region_tax_history (
id serial PRIMARY KEY,
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
old_tax_percent numeric(12,4) NOT NULL,
new_tax_percent numeric(12,4) NOT NULL,
setter_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS region_tax_history_region_idx
ON falukant_data.region_tax_history (region_id, created_at DESC);
`);
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.political_appointment (
id serial PRIMARY KEY,
appointer_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
target_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
office_type_id integer NOT NULL REFERENCES falukant_type.political_office_type(id) ON DELETE CASCADE,
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
status varchar(32) NOT NULL DEFAULT 'completed',
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at timestamptz NULL,
completed_political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS political_appointment_appointer_idx
ON falukant_data.political_appointment (appointer_character_id, created_at DESC);
CREATE INDEX IF NOT EXISTS political_appointment_target_idx
ON falukant_data.political_appointment (target_character_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS falukant_data.political_appointment;
DROP TABLE IF EXISTS falukant_data.region_tax_history;
DROP TABLE IF EXISTS falukant_data.political_benefit_last_tick;
`);
}
};

View File

@@ -0,0 +1,50 @@
'use strict';
/** Stufe pro politischem Amt (Tageshonorar: base + perRank × hierarchy_level). */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.political_office_type
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_type.political_office_type AS pot
SET hierarchy_level = sub.lvl
FROM (VALUES
('assessor', 1),
('councillor', 1),
('council', 2),
('beadle', 2),
('town-clerk', 2),
('mayor', 3),
('master-builder', 2),
('village-major', 2),
('judge', 3),
('bailif', 3),
('taxman', 2),
('sheriff', 3),
('consultant', 3),
('treasurer', 4),
('hangman', 2),
('territorial-council', 3),
('territorial-council-speaker', 4),
('ruler-consultant', 4),
('state-administrator', 4),
('super-state-administrator', 5),
('governor', 5),
('ministry-helper', 4),
('minister', 5),
('chancellor', 6)
) AS sub(name, lvl)
WHERE pot.name = sub.name;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.political_office_type
DROP COLUMN IF EXISTS hierarchy_level;
`);
}
};

View File

@@ -0,0 +1,32 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS scandal_extra_daily_pct double precision NOT NULL DEFAULT 0;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_scandal_extra_daily_pct_chk
CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100);
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS scandal_extra_daily_pct;
`);
},
};

View File

@@ -0,0 +1,54 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS marriage_satisfaction integer;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_data.relationship_state
SET marriage_satisfaction = 55
WHERE marriage_satisfaction IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ALTER COLUMN marriage_satisfaction SET DEFAULT 55;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ALTER COLUMN marriage_satisfaction SET NOT NULL;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'relationship_state_marriage_satisfaction_check'
AND connamespace = 'falukant_data'::regnamespace
) THEN
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_marriage_satisfaction_check
CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100);
END IF;
END $$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_marriage_satisfaction_check;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS marriage_satisfaction;
`);
}
};

View File

@@ -0,0 +1,49 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_srs_item (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES community."user"(id) ON DELETE CASCADE,
course_id INTEGER NOT NULL REFERENCES community.vocab_course(id) ON DELETE CASCADE,
lesson_id INTEGER NULL REFERENCES community.vocab_course_lesson(id) ON DELETE SET NULL,
item_key VARCHAR(80) NOT NULL,
learning TEXT NOT NULL,
reference TEXT NOT NULL,
direction VARCHAR(8) NOT NULL DEFAULT 'BOTH',
stage INTEGER NOT NULL DEFAULT 0,
interval_days INTEGER NOT NULL DEFAULT 0,
last_reviewed_at TIMESTAMP WITH TIME ZONE NULL,
next_due_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
correct_count INTEGER NOT NULL DEFAULT 0,
wrong_count INTEGER NOT NULL DEFAULT 0,
lapse_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_srs_item_user_key_unique UNIQUE (user_id, item_key)
);
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_due
ON community.vocab_srs_item (user_id, course_id, next_due_at);
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_lesson
ON community.vocab_srs_item (user_id, course_id, lesson_id);
COMMENT ON TABLE community.vocab_srs_item IS
'Nutzerbezogener SRS-Fortschritt pro Vokabel/Phrase aus Sprachkursen.';
COMMENT ON COLUMN community.vocab_srs_item.item_key IS
'Stabiler deterministischer Schlüssel aus Kurs, Lektion und normalisiertem Begriffspaar.';
COMMENT ON COLUMN community.vocab_srs_item.stage IS
'SRS-Stufe. Höhere Stufen bedeuten längere Wiederholungsintervalle.';
COMMENT ON COLUMN community.vocab_srs_item.next_due_at IS
'Zeitpunkt, zu dem das Item wieder fällig ist.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_srs_item;
`);
}
};

View File

@@ -0,0 +1,35 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
// Aktiviere die pgcrypto Erweiterung, die die digest() Funktion bereitstellt
await queryInterface.sequelize.query(`
CREATE EXTENSION IF NOT EXISTS pgcrypto;
`);
await queryInterface.sequelize.query(`
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
BEGIN
NEW.hashed_id = encode(digest(NEW.id::text, 'sha256'), 'hex');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await queryInterface.sequelize.query(`
CREATE TRIGGER update_hashed_id_trigger
BEFORE INSERT OR UPDATE ON community.user
FOR EACH ROW EXECUTE FUNCTION community.update_hashed_id();
`);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
DROP TRIGGER IF EXISTS update_hashed_id_trigger ON community.user;
`);
await queryInterface.sequelize.query(`
DROP FUNCTION IF EXISTS community.update_hashed_id();
`);
}
};