mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('users', 'preferred_currency', {
|
||||
type: Sequelize.CHAR(3),
|
||||
allowNull: false,
|
||||
defaultValue: 'USD',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('users', 'preferred_currency');
|
||||
},
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('plan_prices', {
|
||||
price_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
plan_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false,
|
||||
references: { model: 'tier_plans', key: 'plan_id' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
currency: { type: Sequelize.CHAR(3), allowNull: false },
|
||||
price: { type: Sequelize.DECIMAL(10, 2), allowNull: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
await queryInterface.addConstraint('plan_prices', {
|
||||
fields: ['plan_id', 'currency'],
|
||||
type: 'unique',
|
||||
name: 'uq_plan_prices_plan_currency',
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('plan_prices', ['plan_id'], {
|
||||
name: 'idx_plan_prices_plan_id',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('plan_prices');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('notification_broadcasts', {
|
||||
broadcast_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, unique: true },
|
||||
title: { type: Sequelize.STRING(255), allowNull: false },
|
||||
message: { type: Sequelize.TEXT, allowNull: false },
|
||||
audience: { type: Sequelize.ENUM('admin', 'user', 'both'), allowNull: false },
|
||||
status: { type: Sequelize.ENUM('draft', 'sent', 'archived'), allowNull: false, defaultValue: 'draft' },
|
||||
sent_at: { type: Sequelize.DATE, allowNull: true },
|
||||
recipient_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
createdBy: { type: Sequelize.BIGINT, allowNull: true },
|
||||
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
|
||||
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
deletedAt: { type: Sequelize.DATE, allowNull: true },
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('notification_broadcasts', ['uuid']);
|
||||
await queryInterface.addIndex('notification_broadcasts', ['status']);
|
||||
await queryInterface.addIndex('notification_broadcasts', ['audience']);
|
||||
await queryInterface.addIndex('notification_broadcasts', ['deletedAt']);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('notification_broadcasts');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// CockroachDB: add enum values in place, then rename the column.
|
||||
// Type is named 'notification_broadcast_audience' (matches the explicit CREATE TYPE
|
||||
// used when this table was first created — not Sequelize's default enum_<table>_<col> name).
|
||||
await queryInterface.sequelize.query(`ALTER TYPE public.notification_broadcast_audience ADD VALUE IF NOT EXISTS 'task_list'`);
|
||||
await queryInterface.sequelize.query(`ALTER TYPE public.notification_broadcast_audience ADD VALUE IF NOT EXISTS 'course'`);
|
||||
await queryInterface.sequelize.query(`ALTER TYPE public.notification_broadcast_audience ADD VALUE IF NOT EXISTS 'tier_plan'`);
|
||||
|
||||
await queryInterface.renameColumn('notification_broadcasts', 'audience', 'target_type');
|
||||
|
||||
await queryInterface.addColumn('notification_broadcasts', 'target_id', {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.removeIndex('notification_broadcasts', ['audience']).catch(() => {});
|
||||
await queryInterface.addIndex('notification_broadcasts', ['target_type']);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'target_id');
|
||||
await queryInterface.renameColumn('notification_broadcasts', 'target_type', 'audience');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('cron_notification_settings', {
|
||||
job_name: { type: Sequelize.STRING(64), primaryKey: true },
|
||||
enabled: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
|
||||
schedule: { type: Sequelize.STRING(20), allowNull: false },
|
||||
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('cron_notification_settings');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('advertisements', 'placement', {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('advertisements', ['placement']);
|
||||
|
||||
// Backfill the only two placements that were actually wired up before this
|
||||
// migration (Dashboard hero + popup). Any existing banner/sidebar rows are
|
||||
// left with placement = NULL — dev data, an admin reassigns them via the
|
||||
// edit form once the new Page/Position picker ships.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE advertisements SET placement = 'dashboard.hero' WHERE type = 'hero' AND placement IS NULL
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE advertisements SET placement = 'dashboard.popup' WHERE type = 'popup' AND placement IS NULL
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeIndex('advertisements', ['placement']).catch(() => {});
|
||||
await queryInterface.removeColumn('advertisements', 'placement');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('achievement_definitions', {
|
||||
achievement_definition_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
key: { type: Sequelize.STRING(100), allowNull: false, unique: true },
|
||||
type: { type: Sequelize.ENUM('badge', 'milestone'), allowNull: false, defaultValue: 'badge' },
|
||||
label: { type: Sequelize.STRING(255), allowNull: false },
|
||||
description: { type: Sequelize.TEXT, allowNull: true },
|
||||
icon: { type: Sequelize.STRING(50), allowNull: true },
|
||||
trigger: { type: Sequelize.STRING(30), allowNull: true },
|
||||
is_active: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
|
||||
is_system: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
// Seed the 9 built-in keys that services/achievements.service.js's trigger
|
||||
// functions reference by name — these are marked is_system so they can't be
|
||||
// deleted/renamed from the admin CRUD.
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO achievement_definitions (key, type, label, description, icon, trigger, is_active, is_system, "createdAt", "updatedAt")
|
||||
VALUES
|
||||
('early_access', 'badge', 'Early Access', 'Registered during the Philproperties beta period (before Dec 31, 2026).', 'Star', 'auth', true, true, NOW(), NOW()),
|
||||
('premium_first_time', 'badge', 'Premium Member', 'Purchased a Premium tier plan for the first time.', 'BadgeCheck', 'tier', true, true, NOW(), NOW()),
|
||||
('exclusive_first_time', 'badge', 'Exclusive Member', 'Purchased an Exclusive tier plan for the first time.', 'Medal', 'tier', true, true, NOW(), NOW()),
|
||||
('first_course_completed', 'milestone', 'First Course Completed', 'Completed your very first course on Philproperties.', 'BookOpen', 'course', true, true, NOW(), NOW()),
|
||||
('courses_completed_5', 'milestone', 'Learning Streak', 'Completed 5 courses.', 'Flame', 'course', true, true, NOW(), NOW()),
|
||||
('courses_completed_10', 'milestone', 'Knowledge Builder', 'Completed 10 courses.', 'Zap', 'course', true, true, NOW(), NOW()),
|
||||
('perfect_quiz_score', 'milestone', 'Perfect Score', 'Achieved a perfect score on a quiz.', 'Target', 'course', true, true, NOW(), NOW()),
|
||||
('profile_completed', 'milestone', 'Profile Complete', 'Filled out all personal profile information.', 'Shield', 'profile', true, true, NOW(), NOW()),
|
||||
('first_referral', 'milestone', 'Referral Champion', 'Successfully referred a user to Philproperties.', 'Award', 'social', true, true, NOW(), NOW())
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('achievement_definitions');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('achievements', 'icon', {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('achievements', 'icon');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('email_templates', {
|
||||
email_template_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
type: { type: Sequelize.STRING(100), allowNull: false, unique: true },
|
||||
label: { type: Sequelize.STRING(150), allowNull: false },
|
||||
subject: { type: Sequelize.STRING(255), allowNull: false },
|
||||
html_body: { type: Sequelize.TEXT, allowNull: false },
|
||||
is_system: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
const FONT = 'font-family: Arial, sans-serif; font-size: 14px; color: #000;';
|
||||
const now = new Date();
|
||||
|
||||
// Seed the 8 built-in types that services/email.service.js's sendEmail()
|
||||
// looks up by name — these are marked is_system so they can't be
|
||||
// deleted/renamed from the admin CRUD. Body content only (no <html>/<body>
|
||||
// wrapper) — the header/footer layout stays fixed in code, not admin-editable.
|
||||
await queryInterface.bulkInsert('email_templates', [
|
||||
{
|
||||
type: 'OTP', label: 'OTP Verification', is_system: true,
|
||||
subject: 'Email OTP Verification - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>Please use the One-Time Password (OTP) below to verify your email address. This code is valid for <strong>{{expiryMinutes}} minutes</strong>.</p>
|
||||
<p style="font-size: 28px; font-weight: bold; letter-spacing: 6px;">{{otp}}</p>
|
||||
<p>For security reasons, please do not share this code with anyone. If you did not request this, please contact the administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'WELCOME', label: 'Welcome Email', is_system: true,
|
||||
subject: 'Welcome to STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>We are pleased to welcome you to the STARR System. Your account has been successfully created and is now ready for use.</p>
|
||||
<p>You may now access your dashboard and begin using the available services.</p>
|
||||
<p>We look forward to supporting you.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'PASSWORD_CHANGED', label: 'Password Changed', is_system: true,
|
||||
subject: 'Password Update Confirmation - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>This is to confirm that your account password has been successfully changed.</p>
|
||||
<p>If you did not perform this action, please reset your password immediately or contact support.</p>
|
||||
<p>For your security, we recommend using a strong and unique password.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'ADDED_TO_GROUP', label: 'Added to Group', is_system: true,
|
||||
subject: 'Group Assignment Notification - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>You have been assigned to the group <strong>{{groupName}}</strong> in the STARR System.</p>
|
||||
<p>This assignment grants you access to shared resources and collaboration tools within the group.</p>
|
||||
<p>Please log in to your account to view group details.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'TASK_ASSIGNED', label: 'Task Assigned', is_system: true,
|
||||
subject: 'New Task Assignment - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>You have been assigned a new task in the STARR System.</p>
|
||||
<table style="${FONT}">
|
||||
<tr><td><strong>Task</strong></td><td>{{taskTitle}}</td></tr>
|
||||
<tr><td><strong>Due Date</strong></td><td>{{dueDate}}</td></tr>
|
||||
</table>
|
||||
<p>Kindly ensure completion within the specified timeframe.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'BAN_LIFTED', label: 'Ban Lifted', is_system: true,
|
||||
subject: 'Account Suspension Lifted - STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>We are writing to inform you that the suspension on your account (<strong>{{email}}</strong>) has been lifted effective <strong>{{date}}</strong>.</p>
|
||||
<p>You may now log in and resume access to all services within the STARR System.</p>
|
||||
<p>If you have any concerns, please do not hesitate to contact your administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'BANNED', label: 'Account Banned', is_system: true,
|
||||
subject: 'Account Suspension Notice - STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>Your account (<strong>{{email}}</strong>) has been {{duration_word}} suspended from the STARR System effective <strong>{{date}}</strong>.</p>
|
||||
<table style="${FONT}">
|
||||
<tr><td><strong>Reason</strong></td><td>{{reason}}</td></tr>
|
||||
<tr><td><strong>Duration</strong></td><td>{{duration_label}}</td></tr>
|
||||
</table>
|
||||
<p>During this period, access to all system services has been revoked.{{suspension_note}}</p>
|
||||
<p>If you believe this was made in error, please contact your administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'ADD_STAFF', label: 'Staff Account Created', is_system: true,
|
||||
subject: 'Your Staff Account Has Been Created - STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>Your staff account has been successfully created in the STARR System. Below are your login credentials:</p>
|
||||
<table style="${FONT}">
|
||||
<tr><td><strong>Email</strong></td><td>{{email}}</td></tr>
|
||||
<tr><td><strong>Password</strong></td><td>{{password}}</td></tr>
|
||||
</table>
|
||||
<p>This temporary password is valid for <strong>{{expiryHours}} hours</strong>. You will be required to change it upon first login.</p>
|
||||
<p>If you did not request this account or believe this was created in error, please contact your administrator immediately to have it deactivated.</p>
|
||||
<p>For security, please do not share your credentials with anyone. If you need a new password, contact your administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('email_templates');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('email_templates', 'category', {
|
||||
type: Sequelize.ENUM('announcement', 'advertisement', 'system', 'other'),
|
||||
allowNull: false,
|
||||
defaultValue: 'other',
|
||||
});
|
||||
|
||||
// The 8 built-in (is_system) templates are all account/auth notifications.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE email_templates SET category = 'system' WHERE is_system = true;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('email_templates', 'category');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_templates_category";`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Live `subject`/`html_body` become nullable — a brand-new template can
|
||||
// now exist as a pure draft with no live content at all until it's sent.
|
||||
await queryInterface.changeColumn('email_templates', 'subject', {
|
||||
type: Sequelize.STRING(255), allowNull: true,
|
||||
});
|
||||
await queryInterface.changeColumn('email_templates', 'html_body', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('email_templates', 'status', {
|
||||
type: Sequelize.ENUM('draft', 'sent'), allowNull: false, defaultValue: 'draft',
|
||||
});
|
||||
|
||||
// Edits to a 'sent' template land here first — sendEmail() only ever reads
|
||||
// the live subject/html_body columns, never these — until an admin
|
||||
// explicitly re-sends (publishes), the pending edit can't affect real mail.
|
||||
await queryInterface.addColumn('email_templates', 'draft_subject', {
|
||||
type: Sequelize.STRING(255), allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('email_templates', 'draft_html_body', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('email_templates', 'last_sent_at', {
|
||||
type: Sequelize.DATE, allowNull: true,
|
||||
});
|
||||
|
||||
// Every row created before this migration already had required subject/
|
||||
// html_body — meaning it was already "operating" in the old single-state
|
||||
// world. Backfill them all as sent.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE email_templates SET status = 'sent', last_sent_at = "updatedAt";
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('email_templates', 'last_sent_at');
|
||||
await queryInterface.removeColumn('email_templates', 'draft_html_body');
|
||||
await queryInterface.removeColumn('email_templates', 'draft_subject');
|
||||
await queryInterface.removeColumn('email_templates', 'status');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_templates_status";`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('email_broadcasts', {
|
||||
email_broadcast_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
email_template_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'email_templates', key: 'email_template_id' } },
|
||||
target_type: { type: Sequelize.ENUM('admin', 'user', 'both', 'task_list', 'course', 'tier_plan'), allowNull: false },
|
||||
target_id: { type: Sequelize.STRING(64), allowNull: true },
|
||||
status: { type: Sequelize.ENUM('queued', 'sending', 'completed', 'canceled'), allowNull: false, defaultValue: 'queued' },
|
||||
total_recipients: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
sent_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
failed_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
started_at: { type: Sequelize.DATE, allowNull: true },
|
||||
completed_at: { type: Sequelize.DATE, allowNull: true },
|
||||
createdBy: { type: Sequelize.BIGINT, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('email_broadcasts', ['status']);
|
||||
|
||||
await queryInterface.createTable('email_broadcast_recipients', {
|
||||
email_broadcast_recipient_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
email_broadcast_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'email_broadcasts', key: 'email_broadcast_id' } },
|
||||
user_id: { type: Sequelize.BIGINT, allowNull: true },
|
||||
email: { type: Sequelize.STRING(255), allowNull: false },
|
||||
name: { type: Sequelize.STRING(255), allowNull: true },
|
||||
status: { type: Sequelize.ENUM('pending', 'sent', 'failed'), allowNull: false, defaultValue: 'pending' },
|
||||
error: { type: Sequelize.TEXT, allowNull: true },
|
||||
sent_at: { type: Sequelize.DATE, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
// The cron's core query: "give me the next pending batch for a broadcast".
|
||||
await queryInterface.addIndex('email_broadcast_recipients', ['email_broadcast_id', 'status']);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('email_broadcast_recipients');
|
||||
await queryInterface.dropTable('email_broadcasts');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_broadcast_recipients_status";`);
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_broadcasts_status";`);
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_broadcasts_target_type";`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// The admin editor now authors in Markdown (converted client-side to the
|
||||
// HTML that subject/html_body already require) — these columns retain the
|
||||
// original Markdown purely so reopening a template for editing shows the
|
||||
// human-readable source again instead of the compiled HTML. Templates
|
||||
// created before this migration (all 8 system templates included) have
|
||||
// no Markdown source — html_body/draft_html_body remain hand-written HTML
|
||||
// for them, and the editor falls back to editing that directly.
|
||||
await queryInterface.addColumn('email_templates', 'body_markdown', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('email_templates', 'draft_body_markdown', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('email_templates', 'draft_body_markdown');
|
||||
await queryInterface.removeColumn('email_templates', 'body_markdown');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user