Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
535 lines
23 KiB
JavaScript
535 lines
23 KiB
JavaScript
/**
|
|
* Create tenant-specific notification settings tables
|
|
* This migration moves notification category/subtype settings from global to per-tenant
|
|
*/
|
|
|
|
// Citus reference-table conversion cannot share a transaction with parallel
|
|
// distributed operations from earlier migrations in the same knex batch.
|
|
exports.config = { transaction: false };
|
|
|
|
exports.up = async function(knex) {
|
|
console.log('Creating tenant-specific notification settings tables...');
|
|
|
|
// Check if Citus is enabled - we need to know this to handle FK constraints properly
|
|
const citusFn = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_proc
|
|
WHERE proname = 'create_distributed_table'
|
|
) AS exists;
|
|
`);
|
|
const isCitus = citusFn.rows?.[0]?.exists;
|
|
|
|
// The notification catalogs (categories/subtypes/system_email_templates)
|
|
// are reference tables on production Citus, and the tenant-scoped tables
|
|
// that FK into them (notification_logs, user_notification_preferences,
|
|
// tenant_email_templates) are distributed. A fresh Citus chain reaches this
|
|
// point with all six still local, holding FKs out to already-distributed
|
|
// users/tenants — so neither group can be converted while the cross-FKs tie
|
|
// them into one local graph. Drop the cross-FKs, convert each group to its
|
|
// production shape, then re-add them (distributed -> reference FKs are
|
|
// supported). No-op on prod, where everything is already converted.
|
|
if (isCitus) {
|
|
await knex.raw("SET citus.multi_shard_modify_mode TO 'sequential'");
|
|
|
|
const isReference = async (table) => {
|
|
const { rows } = await knex.raw(
|
|
"SELECT 1 FROM pg_dist_partition WHERE logicalrelid = ?::regclass AND partmethod = 'n' AND repmodel = 't'",
|
|
[table]
|
|
);
|
|
return rows.length > 0;
|
|
};
|
|
const isDistributed = async (table) => {
|
|
const { rows } = await knex.raw(
|
|
"SELECT 1 FROM pg_dist_partition WHERE logicalrelid = ?::regclass AND partmethod = 'h'",
|
|
[table]
|
|
);
|
|
return rows.length > 0;
|
|
};
|
|
|
|
const referenceTables = ['notification_categories', 'notification_subtypes', 'system_email_templates'];
|
|
const distributedTables = ['notification_logs', 'user_notification_preferences', 'tenant_email_templates'];
|
|
|
|
const pendingReference = [];
|
|
for (const t of referenceTables) {
|
|
if (!(await isReference(t))) pendingReference.push(t);
|
|
}
|
|
const pendingDistributed = [];
|
|
for (const t of distributedTables) {
|
|
if (!(await isDistributed(t))) pendingDistributed.push(t);
|
|
}
|
|
|
|
if (pendingReference.length > 0 || pendingDistributed.length > 0) {
|
|
const { rows: crossFks } = await knex.raw(`
|
|
SELECT conrelid::regclass::text AS tbl, conname, pg_get_constraintdef(oid) AS def
|
|
FROM pg_constraint
|
|
WHERE contype = 'f'
|
|
AND conrelid::regclass::text = ANY(ARRAY['notification_logs', 'user_notification_preferences', 'tenant_email_templates'])
|
|
AND confrelid::regclass::text = ANY(ARRAY['notification_categories', 'notification_subtypes', 'system_email_templates'])
|
|
`);
|
|
|
|
for (const fk of crossFks) {
|
|
await knex.raw(`ALTER TABLE ${fk.tbl} DROP CONSTRAINT "${fk.conname}"`);
|
|
}
|
|
// create_reference_table also accepts the citus-local tables that the
|
|
// first conversion auto-creates from the rest of the catalog FK graph.
|
|
for (const t of pendingReference) {
|
|
await knex.raw('SELECT create_reference_table(?)', [t]);
|
|
}
|
|
for (const t of pendingDistributed) {
|
|
await knex.raw("SELECT create_distributed_table(?, 'tenant')", [t]);
|
|
}
|
|
for (const fk of crossFks) {
|
|
await knex.raw(`ALTER TABLE ${fk.tbl} ADD CONSTRAINT "${fk.conname}" ${fk.def}`);
|
|
}
|
|
for (const t of [...pendingReference, ...pendingDistributed]) {
|
|
await knex.raw('SELECT truncate_local_data_after_distributing_table(?::regclass)', [t]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to check if table exists
|
|
const tableExists = async (tableName) => {
|
|
const result = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.tables
|
|
WHERE table_schema = 'public' AND table_name = ?
|
|
) AS exists;
|
|
`, [tableName]);
|
|
return result.rows[0].exists;
|
|
};
|
|
|
|
// Helper function to check if constraint exists
|
|
const constraintExists = async (tableName, constraintName) => {
|
|
const result = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM information_schema.table_constraints
|
|
WHERE table_schema = 'public'
|
|
AND table_name = ?
|
|
AND constraint_name = ?
|
|
) AS exists;
|
|
`, [tableName, constraintName]);
|
|
return result.rows[0].exists;
|
|
};
|
|
|
|
// In Citus, we need to:
|
|
// 1. Create tables WITHOUT foreign keys
|
|
// 2. Distribute the tables
|
|
// 3. Add foreign keys AFTER distribution
|
|
// This avoids "out of shared memory" errors when Citus tries to convert local tables
|
|
|
|
// Create tenant_notification_category_settings (without FKs for Citus)
|
|
if (!await tableExists('tenant_notification_category_settings')) {
|
|
await knex.schema.createTable('tenant_notification_category_settings', table => {
|
|
table.uuid('tenant').notNullable();
|
|
table.uuid('tenant_notification_category_setting_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
|
table.integer('category_id').notNullable();
|
|
table.boolean('is_enabled').notNullable().defaultTo(true);
|
|
table.boolean('is_default_enabled').notNullable().defaultTo(true);
|
|
table.timestamps(true, true);
|
|
|
|
// Composite primary key with tenant first for Citus
|
|
table.primary(['tenant_notification_category_setting_id', 'tenant']);
|
|
|
|
// Foreign keys - only add in non-Citus environments
|
|
// In Citus, these will be added AFTER distribution
|
|
if (!isCitus) {
|
|
table.foreign('tenant').references('tenant').inTable('tenants').onDelete('CASCADE');
|
|
table.foreign('category_id').references('id').inTable('notification_categories').onDelete('CASCADE');
|
|
}
|
|
|
|
// Unique constraint on tenant + category_id
|
|
table.unique(['tenant', 'category_id']);
|
|
});
|
|
console.log(' ✓ Created tenant_notification_category_settings');
|
|
} else {
|
|
console.log(' - tenant_notification_category_settings already exists');
|
|
}
|
|
|
|
// Create tenant_notification_subtype_settings (without FKs for Citus)
|
|
if (!await tableExists('tenant_notification_subtype_settings')) {
|
|
await knex.schema.createTable('tenant_notification_subtype_settings', table => {
|
|
table.uuid('tenant').notNullable();
|
|
table.uuid('tenant_notification_subtype_setting_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
|
table.integer('subtype_id').notNullable();
|
|
table.boolean('is_enabled').notNullable().defaultTo(true);
|
|
table.boolean('is_default_enabled').notNullable().defaultTo(true);
|
|
table.timestamps(true, true);
|
|
|
|
// Composite primary key with tenant first for Citus
|
|
table.primary(['tenant_notification_subtype_setting_id', 'tenant']);
|
|
|
|
// Foreign keys - only add in non-Citus environments
|
|
if (!isCitus) {
|
|
table.foreign('tenant').references('tenant').inTable('tenants').onDelete('CASCADE');
|
|
table.foreign('subtype_id').references('id').inTable('notification_subtypes').onDelete('CASCADE');
|
|
}
|
|
|
|
// Unique constraint on tenant + subtype_id
|
|
table.unique(['tenant', 'subtype_id']);
|
|
});
|
|
console.log(' ✓ Created tenant_notification_subtype_settings');
|
|
} else {
|
|
console.log(' - tenant_notification_subtype_settings already exists');
|
|
}
|
|
|
|
// Create tenant_internal_notification_category_settings (without FKs for Citus)
|
|
if (!await tableExists('tenant_internal_notification_category_settings')) {
|
|
await knex.schema.createTable('tenant_internal_notification_category_settings', table => {
|
|
table.uuid('tenant').notNullable();
|
|
table.uuid('tenant_internal_notification_category_setting_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
|
table.integer('category_id').notNullable();
|
|
table.boolean('is_enabled').notNullable().defaultTo(true);
|
|
table.boolean('is_default_enabled').notNullable().defaultTo(true);
|
|
table.timestamps(true, true);
|
|
|
|
// Composite primary key with tenant first for Citus
|
|
table.primary(['tenant_internal_notification_category_setting_id', 'tenant']);
|
|
|
|
// Foreign keys - only add in non-Citus environments
|
|
if (!isCitus) {
|
|
table.foreign('tenant').references('tenant').inTable('tenants').onDelete('CASCADE');
|
|
table.foreign('category_id')
|
|
.references('internal_notification_category_id')
|
|
.inTable('internal_notification_categories')
|
|
.onDelete('CASCADE');
|
|
}
|
|
|
|
// Unique constraint on tenant + category_id
|
|
table.unique(['tenant', 'category_id']);
|
|
});
|
|
console.log(' ✓ Created tenant_internal_notification_category_settings');
|
|
} else {
|
|
console.log(' - tenant_internal_notification_category_settings already exists');
|
|
}
|
|
|
|
// Create tenant_internal_notification_subtype_settings (without FKs for Citus)
|
|
if (!await tableExists('tenant_internal_notification_subtype_settings')) {
|
|
await knex.schema.createTable('tenant_internal_notification_subtype_settings', table => {
|
|
table.uuid('tenant').notNullable();
|
|
table.uuid('tenant_internal_notification_subtype_setting_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
|
|
table.integer('subtype_id').notNullable();
|
|
table.boolean('is_enabled').notNullable().defaultTo(true);
|
|
table.boolean('is_default_enabled').notNullable().defaultTo(true);
|
|
table.timestamps(true, true);
|
|
|
|
// Composite primary key with tenant first for Citus
|
|
table.primary(['tenant_internal_notification_subtype_setting_id', 'tenant']);
|
|
|
|
// Foreign keys - only add in non-Citus environments
|
|
if (!isCitus) {
|
|
table.foreign('tenant').references('tenant').inTable('tenants').onDelete('CASCADE');
|
|
table.foreign('subtype_id')
|
|
.references('internal_notification_subtype_id')
|
|
.inTable('internal_notification_subtypes')
|
|
.onDelete('CASCADE');
|
|
}
|
|
|
|
// Unique constraint on tenant + subtype_id
|
|
table.unique(['tenant', 'subtype_id']);
|
|
});
|
|
console.log(' ✓ Created tenant_internal_notification_subtype_settings');
|
|
} else {
|
|
console.log(' - tenant_internal_notification_subtype_settings already exists');
|
|
}
|
|
|
|
// Create indexes for lookups (IF NOT EXISTS)
|
|
await knex.raw(`
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_notification_category_settings_lookup
|
|
ON tenant_notification_category_settings(tenant, category_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_notification_subtype_settings_lookup
|
|
ON tenant_notification_subtype_settings(tenant, subtype_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_internal_notification_category_settings_lookup
|
|
ON tenant_internal_notification_category_settings(tenant, category_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_internal_notification_subtype_settings_lookup
|
|
ON tenant_internal_notification_subtype_settings(tenant, subtype_id);
|
|
`);
|
|
console.log(' ✓ Created lookup indexes');
|
|
|
|
// Handle Citus distribution and FK constraints
|
|
if (isCitus) {
|
|
console.log(' Citus detected, handling distribution and foreign keys...');
|
|
|
|
// Note: We skip making internal_notification_* tables reference tables
|
|
// because they have complex FK relationships that prevent conversion.
|
|
// The FK constraints to these tables will also be skipped in Citus.
|
|
|
|
// Distribute the new tenant settings tables
|
|
const tables = [
|
|
'tenant_notification_category_settings',
|
|
'tenant_notification_subtype_settings',
|
|
'tenant_internal_notification_category_settings',
|
|
'tenant_internal_notification_subtype_settings'
|
|
];
|
|
|
|
for (const tableName of tables) {
|
|
// Check if table is already distributed
|
|
const isDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = '${tableName}'::regclass
|
|
) AS distributed;
|
|
`);
|
|
|
|
if (!isDistributed.rows[0].distributed) {
|
|
await knex.raw(`SELECT create_distributed_table('${tableName}', 'tenant')`);
|
|
console.log(` ✓ Distributed ${tableName}`);
|
|
} else {
|
|
console.log(` - ${tableName} already distributed`);
|
|
}
|
|
}
|
|
|
|
// Now add foreign key constraints AFTER distribution (idempotent)
|
|
console.log(' Adding foreign key constraints after distribution...');
|
|
|
|
// Define FK constraints to add
|
|
// Note: We skip FK constraints to internal_notification_* tables in Citus
|
|
// because those tables cannot be converted to reference tables
|
|
const fkConstraints = [
|
|
// FK to tenants table (distributed table)
|
|
{
|
|
table: 'tenant_notification_category_settings',
|
|
constraint: 'tenant_notification_category_settings_tenant_foreign',
|
|
sql: `ALTER TABLE tenant_notification_category_settings
|
|
ADD CONSTRAINT tenant_notification_category_settings_tenant_foreign
|
|
FOREIGN KEY (tenant) REFERENCES tenants(tenant) ON DELETE CASCADE`
|
|
},
|
|
{
|
|
table: 'tenant_notification_subtype_settings',
|
|
constraint: 'tenant_notification_subtype_settings_tenant_foreign',
|
|
sql: `ALTER TABLE tenant_notification_subtype_settings
|
|
ADD CONSTRAINT tenant_notification_subtype_settings_tenant_foreign
|
|
FOREIGN KEY (tenant) REFERENCES tenants(tenant) ON DELETE CASCADE`
|
|
},
|
|
{
|
|
table: 'tenant_internal_notification_category_settings',
|
|
constraint: 'tenant_internal_notification_category_settings_tenant_foreign',
|
|
sql: `ALTER TABLE tenant_internal_notification_category_settings
|
|
ADD CONSTRAINT tenant_internal_notification_category_settings_tenant_foreign
|
|
FOREIGN KEY (tenant) REFERENCES tenants(tenant) ON DELETE CASCADE`
|
|
},
|
|
{
|
|
table: 'tenant_internal_notification_subtype_settings',
|
|
constraint: 'tenant_internal_notification_subtype_settings_tenant_foreign',
|
|
sql: `ALTER TABLE tenant_internal_notification_subtype_settings
|
|
ADD CONSTRAINT tenant_internal_notification_subtype_settings_tenant_foreign
|
|
FOREIGN KEY (tenant) REFERENCES tenants(tenant) ON DELETE CASCADE`
|
|
},
|
|
// FK to reference tables (notification_categories, notification_subtypes)
|
|
// These are already reference tables in Citus
|
|
{
|
|
table: 'tenant_notification_category_settings',
|
|
constraint: 'tenant_notification_category_settings_category_id_foreign',
|
|
sql: `ALTER TABLE tenant_notification_category_settings
|
|
ADD CONSTRAINT tenant_notification_category_settings_category_id_foreign
|
|
FOREIGN KEY (category_id) REFERENCES notification_categories(id) ON DELETE CASCADE`
|
|
},
|
|
{
|
|
table: 'tenant_notification_subtype_settings',
|
|
constraint: 'tenant_notification_subtype_settings_subtype_id_foreign',
|
|
sql: `ALTER TABLE tenant_notification_subtype_settings
|
|
ADD CONSTRAINT tenant_notification_subtype_settings_subtype_id_foreign
|
|
FOREIGN KEY (subtype_id) REFERENCES notification_subtypes(id) ON DELETE CASCADE`
|
|
}
|
|
// Note: FK constraints to internal_notification_categories and internal_notification_subtypes
|
|
// are intentionally skipped in Citus - referential integrity is enforced at application level
|
|
];
|
|
|
|
for (const fk of fkConstraints) {
|
|
if (!await constraintExists(fk.table, fk.constraint)) {
|
|
await knex.raw(fk.sql);
|
|
console.log(` ✓ Added ${fk.constraint}`);
|
|
} else {
|
|
console.log(` - ${fk.constraint} already exists`);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
console.log(' Citus not detected, skipping table distribution');
|
|
}
|
|
|
|
// Seed tenant settings from current global values
|
|
// Skip seeding if data already exists (idempotent)
|
|
console.log('Seeding tenant settings from global values...');
|
|
|
|
// Check if seeding is needed (any existing data means we skip)
|
|
const existingCategorySettings = await knex('tenant_notification_category_settings').first();
|
|
if (existingCategorySettings) {
|
|
console.log(' - Seeding skipped: data already exists');
|
|
} else {
|
|
// Get all tenants
|
|
const tenants = await knex('tenants').select('tenant');
|
|
console.log(` Found ${tenants.length} tenants`);
|
|
|
|
if (tenants.length > 0) {
|
|
// Seed notification_categories
|
|
const categories = await knex('notification_categories')
|
|
.select('id', 'is_enabled', 'is_default_enabled');
|
|
|
|
if (categories.length > 0) {
|
|
const categorySettings = [];
|
|
for (const tenant of tenants) {
|
|
for (const category of categories) {
|
|
categorySettings.push({
|
|
tenant: tenant.tenant,
|
|
tenant_notification_category_setting_id: knex.raw('gen_random_uuid()'),
|
|
category_id: category.id,
|
|
is_enabled: category.is_enabled,
|
|
is_default_enabled: category.is_default_enabled,
|
|
created_at: knex.fn.now(),
|
|
updated_at: knex.fn.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Insert in batches of 100
|
|
for (let i = 0; i < categorySettings.length; i += 100) {
|
|
const batch = categorySettings.slice(i, i + 100);
|
|
await knex('tenant_notification_category_settings').insert(batch);
|
|
}
|
|
console.log(` ✓ Seeded ${categorySettings.length} category settings`);
|
|
}
|
|
|
|
// Seed notification_subtypes
|
|
const subtypes = await knex('notification_subtypes')
|
|
.select('id', 'is_enabled', 'is_default_enabled');
|
|
|
|
if (subtypes.length > 0) {
|
|
const subtypeSettings = [];
|
|
for (const tenant of tenants) {
|
|
for (const subtype of subtypes) {
|
|
subtypeSettings.push({
|
|
tenant: tenant.tenant,
|
|
tenant_notification_subtype_setting_id: knex.raw('gen_random_uuid()'),
|
|
subtype_id: subtype.id,
|
|
is_enabled: subtype.is_enabled,
|
|
is_default_enabled: subtype.is_default_enabled,
|
|
created_at: knex.fn.now(),
|
|
updated_at: knex.fn.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Insert in batches of 100
|
|
for (let i = 0; i < subtypeSettings.length; i += 100) {
|
|
const batch = subtypeSettings.slice(i, i + 100);
|
|
await knex('tenant_notification_subtype_settings').insert(batch);
|
|
}
|
|
console.log(` ✓ Seeded ${subtypeSettings.length} subtype settings`);
|
|
}
|
|
|
|
// Seed internal_notification_categories
|
|
const internalCategories = await knex('internal_notification_categories')
|
|
.select('internal_notification_category_id', 'is_enabled', 'is_default_enabled');
|
|
|
|
if (internalCategories.length > 0) {
|
|
const internalCategorySettings = [];
|
|
for (const tenant of tenants) {
|
|
for (const category of internalCategories) {
|
|
internalCategorySettings.push({
|
|
tenant: tenant.tenant,
|
|
tenant_internal_notification_category_setting_id: knex.raw('gen_random_uuid()'),
|
|
category_id: category.internal_notification_category_id,
|
|
is_enabled: category.is_enabled,
|
|
is_default_enabled: category.is_default_enabled,
|
|
created_at: knex.fn.now(),
|
|
updated_at: knex.fn.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Insert in batches of 100
|
|
for (let i = 0; i < internalCategorySettings.length; i += 100) {
|
|
const batch = internalCategorySettings.slice(i, i + 100);
|
|
await knex('tenant_internal_notification_category_settings').insert(batch);
|
|
}
|
|
console.log(` ✓ Seeded ${internalCategorySettings.length} internal category settings`);
|
|
}
|
|
|
|
// Seed internal_notification_subtypes
|
|
const internalSubtypes = await knex('internal_notification_subtypes')
|
|
.select('internal_notification_subtype_id', 'is_enabled', 'is_default_enabled');
|
|
|
|
if (internalSubtypes.length > 0) {
|
|
const internalSubtypeSettings = [];
|
|
for (const tenant of tenants) {
|
|
for (const subtype of internalSubtypes) {
|
|
internalSubtypeSettings.push({
|
|
tenant: tenant.tenant,
|
|
tenant_internal_notification_subtype_setting_id: knex.raw('gen_random_uuid()'),
|
|
subtype_id: subtype.internal_notification_subtype_id,
|
|
is_enabled: subtype.is_enabled,
|
|
is_default_enabled: subtype.is_default_enabled,
|
|
created_at: knex.fn.now(),
|
|
updated_at: knex.fn.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Insert in batches of 100
|
|
for (let i = 0; i < internalSubtypeSettings.length; i += 100) {
|
|
const batch = internalSubtypeSettings.slice(i, i + 100);
|
|
await knex('tenant_internal_notification_subtype_settings').insert(batch);
|
|
}
|
|
console.log(` ✓ Seeded ${internalSubtypeSettings.length} internal subtype settings`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset global tables to all enabled (they become reference data)
|
|
console.log('Resetting global tables to enabled state...');
|
|
await knex('notification_categories').update({
|
|
is_enabled: true,
|
|
is_default_enabled: true,
|
|
updated_at: knex.fn.now()
|
|
});
|
|
await knex('notification_subtypes').update({
|
|
is_enabled: true,
|
|
is_default_enabled: true,
|
|
updated_at: knex.fn.now()
|
|
});
|
|
await knex('internal_notification_categories').update({
|
|
is_enabled: true,
|
|
is_default_enabled: true,
|
|
updated_at: knex.fn.now()
|
|
});
|
|
await knex('internal_notification_subtypes').update({
|
|
is_enabled: true,
|
|
is_default_enabled: true,
|
|
updated_at: knex.fn.now()
|
|
});
|
|
console.log(' ✓ Global tables reset to enabled');
|
|
|
|
console.log('Migration completed successfully!');
|
|
};
|
|
|
|
exports.down = async function(knex) {
|
|
console.log('Reverting tenant-specific notification settings tables...');
|
|
|
|
// Drop indexes first
|
|
await knex.raw(`
|
|
DROP INDEX IF EXISTS idx_tenant_internal_notification_subtype_settings_lookup;
|
|
DROP INDEX IF EXISTS idx_tenant_internal_notification_category_settings_lookup;
|
|
DROP INDEX IF EXISTS idx_tenant_notification_subtype_settings_lookup;
|
|
DROP INDEX IF EXISTS idx_tenant_notification_category_settings_lookup;
|
|
`);
|
|
console.log(' ✓ Dropped indexes');
|
|
|
|
// Drop tables in reverse dependency order
|
|
await knex.schema.dropTableIfExists('tenant_internal_notification_subtype_settings');
|
|
console.log(' ✓ Dropped tenant_internal_notification_subtype_settings');
|
|
|
|
await knex.schema.dropTableIfExists('tenant_internal_notification_category_settings');
|
|
console.log(' ✓ Dropped tenant_internal_notification_category_settings');
|
|
|
|
await knex.schema.dropTableIfExists('tenant_notification_subtype_settings');
|
|
console.log(' ✓ Dropped tenant_notification_subtype_settings');
|
|
|
|
await knex.schema.dropTableIfExists('tenant_notification_category_settings');
|
|
console.log(' ✓ Dropped tenant_notification_category_settings');
|
|
|
|
console.log('Rollback completed successfully!');
|
|
};
|