feat(deploy): update deployment workflow and migration paths
Some checks failed
Deploy to production / deploy (push) Has been cancelled
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:
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
132
backend/migrations-archive/20260115000000-add-vocab-courses.cjs
Normal file
132
backend/migrations-archive/20260115000000-add-vocab-courses.cjs
Normal 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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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');
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
@@ -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' });
|
||||
},
|
||||
};
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
@@ -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(() => {});
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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 $$;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
35
backend/migrations-archive/add_trigger_for_hashedId.cjs
Normal file
35
backend/migrations-archive/add_trigger_for_hashedId.cjs
Normal 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();
|
||||
`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user