mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
// Re-categorizes advertisement placements: Hero stays on Dashboard, Banner
|
||||
// consolidates onto Tier Plans + Course Details. Popup (dashboard.popup) and
|
||||
// Sidebar (course_details.sidebar) are retired as formats, and the Courses
|
||||
// list banner (course_list.banner) is dropped along with them since the new
|
||||
// registry only offers Dashboard / Tier Plans / Course Details.
|
||||
//
|
||||
// Existing rows on a retired placement are archived (soft-deleted) rather
|
||||
// than deleted outright — they still show up in Archived Advertisements for
|
||||
// an admin to review/permanently delete if desired.
|
||||
const RETIRED_PLACEMENTS = ['dashboard.popup', 'course_list.banner', 'course_details.sidebar'];
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE advertisements SET placement = 'tier_plans.banner' WHERE placement = 'plans.banner'
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE advertisements
|
||||
SET "deletedAt" = NOW()
|
||||
WHERE placement IN (:retired) AND "deletedAt" IS NULL
|
||||
`, {
|
||||
replacements: { retired: RETIRED_PLACEMENTS },
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE advertisements SET placement = 'plans.banner' WHERE placement = 'tier_plans.banner'
|
||||
`);
|
||||
// Retired-placement rows that were auto-archived by `up` are intentionally
|
||||
// left archived — there's no reliable way to distinguish them from ads an
|
||||
// admin archived manually in the meantime.
|
||||
},
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
// Supports the new advertisement creation wizard:
|
||||
// - content_mode: "image" (image only) vs "content" (badge/headline/
|
||||
// description/CTAs alongside the image) — an explicit admin choice,
|
||||
// decoupled from placement/format. Added as STRING + an explicit CHECK
|
||||
// constraint (addColumn can't create a native enum type on this
|
||||
// CockroachDB instance, unlike createTable) — matching the pattern used
|
||||
// in 20260710000001-add-status-to-courses.js. The Sequelize model still
|
||||
// declares it as DataTypes.ENUM for app-level validation; the column
|
||||
// itself is a plain string.
|
||||
// - redirect_link: where the ad as a whole links to when clicked with no
|
||||
// CTA of its own (banners/full-image ads have no CTA row).
|
||||
// - landing_page: when no redirect_link is given, an internally-authored
|
||||
// landing page (title/description/body/links) the ad's own click-through
|
||||
// resolves to instead (see GET /api/client/advertisements/uuid/:uuid and
|
||||
// the /ads/:uuid client route).
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('advertisements', 'content_mode', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'image',
|
||||
});
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE advertisements ADD CONSTRAINT check_content_mode
|
||||
CHECK (content_mode IN ('image', 'content'))
|
||||
`);
|
||||
|
||||
await queryInterface.addColumn('advertisements', 'redirect_link', {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('advertisements', 'landing_page', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Existing rows already have real content (headline/description/ctas) —
|
||||
// treat them as "content" mode rather than defaulting to "image".
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE advertisements
|
||||
SET content_mode = 'content'
|
||||
WHERE headline IS NOT NULL AND headline <> ''
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('advertisements', 'landing_page');
|
||||
await queryInterface.removeColumn('advertisements', 'redirect_link');
|
||||
await queryInterface.sequelize.query(`ALTER TABLE advertisements DROP CONSTRAINT IF EXISTS check_content_mode`);
|
||||
await queryInterface.removeColumn('advertisements', 'content_mode');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
// Reverts the notification_templates feature (20260703000008 +
|
||||
// 20260709000004) — admin-editable notification wording has been moved back
|
||||
// to hardcoded build() functions in data/notifications.data.js.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.dropTable('notification_templates');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_notification_templates_scope";`);
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_notification_templates_status";`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('notification_templates', {
|
||||
notification_template_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
type: { type: Sequelize.STRING(100), allowNull: false, unique: true },
|
||||
notify_type: { type: Sequelize.STRING(64), allowNull: false },
|
||||
scope: { type: Sequelize.ENUM('admin', 'user', 'both'), allowNull: false },
|
||||
label: { type: Sequelize.STRING(150), allowNull: false },
|
||||
status: { type: Sequelize.ENUM('draft', 'sent'), allowNull: false, defaultValue: 'draft' },
|
||||
title: { type: Sequelize.STRING(255), allowNull: true },
|
||||
message: { type: Sequelize.TEXT, allowNull: true },
|
||||
draft_title: { type: Sequelize.STRING(255), allowNull: true },
|
||||
draft_message: { type: Sequelize.TEXT, allowNull: true },
|
||||
last_sent_at: { type: Sequelize.DATE, allowNull: true },
|
||||
is_system: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('notification_broadcasts', 'color', {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'indigo',
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('notification_broadcasts', 'link_label', {
|
||||
type: Sequelize.STRING(60),
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('admin_notifications', 'color', {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'indigo',
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('user_notifications', 'color', {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'indigo',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('user_notifications', 'color');
|
||||
await queryInterface.removeColumn('admin_notifications', 'color');
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'link_label');
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'color');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('notification_broadcasts', 'start_date', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('notification_broadcasts', 'end_date', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('admin_notifications', 'start_date', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('admin_notifications', 'end_date', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('user_notifications', 'start_date', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('user_notifications', 'end_date', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('user_notifications', 'end_date');
|
||||
await queryInterface.removeColumn('user_notifications', 'start_date');
|
||||
await queryInterface.removeColumn('admin_notifications', 'end_date');
|
||||
await queryInterface.removeColumn('admin_notifications', 'start_date');
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'end_date');
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'start_date');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('admin_notifications', 'broadcast_id', {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: true,
|
||||
references: { model: 'notification_broadcasts', key: 'broadcast_id' },
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('user_notifications', 'broadcast_id', {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: true,
|
||||
references: { model: 'notification_broadcasts', key: 'broadcast_id' },
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('user_notifications', 'broadcast_id');
|
||||
await queryInterface.removeColumn('admin_notifications', 'broadcast_id');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Single shared banner image for the rotating sticky announcement bar —
|
||||
// one image for all (up to 3) concurrently-active announcements, not one
|
||||
// per announcement. Enforced as a singleton in application code (always
|
||||
// read/written as id=1), same pragmatic approach as the "no need for a
|
||||
// dedicated Banner entity" ask — just a one-row setting.
|
||||
await queryInterface.createTable('sticky_banner_settings', {
|
||||
id: {
|
||||
type: Sequelize.SMALLINT,
|
||||
primaryKey: true,
|
||||
defaultValue: 1,
|
||||
},
|
||||
image_asset_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: true,
|
||||
references: { model: 'assets', key: 'asset_id' },
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
updatedBy: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('sticky_banner_settings');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user