lilith-platform.live/codebase/@features/admin/backend-api/src/migrate.ts
2026-04-18 22:02:24 -07:00

480 lines
22 KiB
TypeScript

/**
* Migration script — populate SQLite from static data.ts + destinations.ts
*
* Reads the providerData from quinn.www data.ts and inserts all records
* into the admin SQLite database. Idempotent: clears existing data before insert.
*
* Usage: bun run src/migrate.ts
*/
import { initSchema, getDb, touchLastModified } from './db';
import { logger } from './logger';
async function main(): Promise<void> {
initSchema();
const db = getDb();
logger.info('Loading static provider data...');
// Dynamic import of the static data modules
const dataModule = await import(
'../../../../../deployments/@domains/quinn.www/root/src/data'
);
const data = dataModule.providerData;
const destModule = await import(
'../../../../../deployments/@domains/quinn.www/root/src/destinations'
);
const destinations = destModule.destinations;
const specModule = await import(
'../../../../../deployments/@domains/quinn.www/root/src/specialties'
);
const specialties = specModule.specialties;
logger.info('Starting migration...');
db.exec('BEGIN TRANSACTION');
try {
// Clear existing data (idempotent)
const tables = [
'identity', 'physical', 'contact', 'about',
'rate_sections', 'rate_entries',
'tour_stops', 'gallery_items',
'policy_sections', 'policy_items',
'etiquette_sections', 'etiquette_items',
'activity_menus', 'destinations', 'specialties',
];
for (const table of tables) {
db.exec(`DELETE FROM ${table}`);
}
// Identity
db.prepare(
'INSERT INTO identity (id, name, pronouns, gender, location, incall_city, tagline, secondary_locations, languages) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)',
).run(
data.identity.name,
data.identity.pronouns,
data.identity.gender,
data.identity.location,
data.identity.incallCity ?? null,
data.identity.tagline,
JSON.stringify(data.identity.secondaryLocations),
JSON.stringify(data.identity.languages),
);
// Physical
db.prepare(
'INSERT INTO physical (id, age, height, body_type, ethnicity, hair_color, eye_color, cup_size, additional) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)',
).run(
data.physical.age,
data.physical.height,
data.physical.bodyType,
data.physical.ethnicity,
data.physical.hairColor,
data.physical.eyeColor,
data.physical.cupSize,
JSON.stringify(data.physical.additional),
);
// Contact
db.prepare(
'INSERT INTO contact (id, phone, whatsapp, email, instagram, twitter, threads, snapchat, youtube, onlyfans, transfans, fansly, loyalfans, fancentro, fantime, tryst, communication_note, response_time, availability_note, payment_methods) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
).run(
data.contact.phone,
data.contact.whatsapp ?? null,
data.contact.email ?? null,
data.contact.instagram ?? null,
data.contact.twitter ?? null,
data.contact.threads ?? null,
data.contact.snapchat ?? null,
data.contact.youtube ?? null,
data.contact.onlyfans ?? null,
data.contact.transfans ?? null,
data.contact.fansly ?? null,
data.contact.loyalfans ?? null,
data.contact.fancentro ?? null,
data.contact.fantime ?? null,
data.contact.tryst ?? null,
data.contact.communicationNote,
data.contact.responseTime,
data.contact.availabilityNote ?? null,
JSON.stringify(data.contact.paymentMethods),
);
// About
db.prepare(
'INSERT INTO about (id, bio, personality, available_for, available_to) VALUES (1, ?, ?, ?, ?)',
).run(
data.about.bio,
JSON.stringify(data.about.personality),
JSON.stringify(data.about.availableFor),
JSON.stringify(data.about.availableTo),
);
// Activity menus
if (data.about.activities) {
const actStmt = db.prepare('INSERT INTO activity_menus (category, items, sort_order) VALUES (?, ?, ?)');
for (let i = 0; i < data.about.activities.length; i++) {
const act = data.about.activities[i];
actStmt.run(act.category, JSON.stringify(act.items), i);
}
}
// Rate sections
const sectionStmt = db.prepare(
'INSERT INTO rate_sections (section_type, title, description, sort_order) VALUES (?, ?, ?, ?)',
);
const entryStmt = db.prepare(
'INSERT INTO rate_entries (section_id, service, duration, price, price_max, description, notes, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
);
let sectionOrder = 0;
for (const section of data.rates) {
const type = section.title.toLowerCase().includes('out') ? 'outcall' : 'incall';
const result = sectionStmt.run(type, section.title, section.description ?? null, sectionOrder++);
const sectionId = Number(result.lastInsertRowid);
for (let j = 0; j < section.entries.length; j++) {
const e = section.entries[j];
entryStmt.run(sectionId, e.service, e.duration ?? null, e.price, e.priceMax ?? null, e.description ?? null, e.notes ?? null, j);
}
}
// Add-Ons
const addOnResult = sectionStmt.run('addons', 'Add-Ons', data.addOns.description ?? null, sectionOrder++);
const addOnId = Number(addOnResult.lastInsertRowid);
for (let j = 0; j < data.addOns.entries.length; j++) {
const e = data.addOns.entries[j];
entryStmt.run(addOnId, e.service, e.duration ?? null, e.price, e.priceMax ?? null, e.description ?? null, e.notes ?? null, j);
}
// Touring Packages
const tourResult = sectionStmt.run('touring', 'Touring Packages (FMTY)', data.touringPackages.description ?? null, sectionOrder++);
const tourSectionId = Number(tourResult.lastInsertRowid);
for (let j = 0; j < data.touringPackages.entries.length; j++) {
const e = data.touringPackages.entries[j];
entryStmt.run(tourSectionId, e.service, e.duration ?? null, e.price, e.priceMax ?? null, e.description ?? null, e.notes ?? null, j);
}
// Online Services
const onlineResult = sectionStmt.run('online', 'Online Services', data.onlineServices.description ?? null, sectionOrder++);
const onlineId = Number(onlineResult.lastInsertRowid);
for (let j = 0; j < data.onlineServices.entries.length; j++) {
const e = data.onlineServices.entries[j];
entryStmt.run(onlineId, e.service, e.duration ?? null, e.price, e.priceMax ?? null, e.description ?? null, e.notes ?? null, j);
}
// Tour stops
const tourStopStmt = db.prepare(
'INSERT INTO tour_stops (city, state, country, start_date, end_date, status, availability_note, pricing_tiers, notes, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
);
for (let i = 0; i < data.tour.length; i++) {
const t = data.tour[i];
tourStopStmt.run(
t.city, t.state, t.country ?? 'USA',
t.startDate, t.endDate,
t.bookingStatus,
t.availabilityNote ?? null,
t.pricingTiers ? JSON.stringify(t.pricingTiers) : null,
t.notes ?? null,
i,
);
}
// Gallery
const galleryStmt = db.prepare(
'INSERT INTO gallery_items (filename, alt, category, featured, webp_filename, intrinsic_width, intrinsic_height, protection_status, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
);
for (let i = 0; i < data.gallery.length; i++) {
const g = data.gallery[i];
const filename = g.src.split('/').pop() ?? g.src;
const webpFilename = g.webpSrc?.split('/').pop() ?? null;
galleryStmt.run(
filename, g.alt, g.category ?? null, g.featured ? 1 : 0,
webpFilename, g.intrinsicWidth ?? null, g.intrinsicHeight ?? null,
'protected', i,
);
}
// Policies
const policySectionStmt = db.prepare('INSERT INTO policy_sections (title, sort_order) VALUES (?, ?)');
const policyItemStmt = db.prepare('INSERT INTO policy_items (section_id, label, detail, sort_order) VALUES (?, ?, ?, ?)');
for (let i = 0; i < data.policies.length; i++) {
const s = data.policies[i];
const result = policySectionStmt.run(s.title, i);
const sId = Number(result.lastInsertRowid);
for (let j = 0; j < s.items.length; j++) {
policyItemStmt.run(sId, s.items[j].label, s.items[j].detail, j);
}
}
// Etiquette
const etiquetteSectionStmt = db.prepare('INSERT INTO etiquette_sections (title, sort_order) VALUES (?, ?)');
const etiquetteItemStmt = db.prepare('INSERT INTO etiquette_items (section_id, label, detail, cta_href, cta_text, sort_order) VALUES (?, ?, ?, ?, ?, ?)');
for (let i = 0; i < data.etiquette.length; i++) {
const s = data.etiquette[i];
const result = etiquetteSectionStmt.run(s.title, i);
const sId = Number(result.lastInsertRowid);
for (let j = 0; j < s.items.length; j++) {
const item = s.items[j];
etiquetteItemStmt.run(sId, item.label, item.detail ?? null, item.ctaHref ?? null, item.ctaText ?? null, j);
}
}
// Destinations
const destStmt = db.prepare(
'INSERT INTO destinations (slug, city, country, region, fmty_tier, meta_title, meta_description, headline, intro, linked_tour_stop, experiences, note, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
);
for (let i = 0; i < destinations.length; i++) {
const d = destinations[i];
destStmt.run(
d.slug, d.city, d.country, d.region ?? null, d.fmtyTier,
d.metaTitle, d.metaDescription, d.headline, d.intro,
d.linkedTourStop ? 1 : 0,
JSON.stringify(d.experiences ?? []),
d.note ?? null, i,
);
}
// Specialties
const specStmt = db.prepare(
'INSERT INTO specialties (category_slug, category_name, category_meta_title, category_meta_description, category_intro, slug, name, meta_title, meta_description, headline, intro, includes, note, related_rate_type, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
);
let specOrder = 0;
for (const cat of specialties) {
for (const item of cat.items) {
specStmt.run(
cat.slug, cat.name, cat.metaTitle, cat.metaDescription, cat.intro,
item.slug, item.name, item.metaTitle, item.metaDescription, item.headline, item.intro,
item.includes ? JSON.stringify(item.includes) : null,
item.note ?? null,
item.relatedRateType ?? null,
specOrder++,
);
}
}
// Site Text — seed all default UI strings
const textStmt = db.prepare(
"INSERT INTO site_text (namespace, key, value) VALUES (?, ?, ?) ON CONFLICT(namespace, key) DO NOTHING",
);
const siteTextDefaults: [string, string, string][] = [
// nav
['nav', 'home', 'Home'],
['nav', 'gallery', 'Gallery'],
['nav', 'rates', 'Rates'],
['nav', 'tour', 'Tour'],
['nav', 'about', 'About'],
['nav', 'links', 'Links'],
['nav', 'book', 'Book'],
['nav', 'contact', 'Contact'],
// hero
['hero', 'badge_available', 'Available'],
['hero', 'badge_here_now', 'Here Now'],
['hero', 'badge_next_stop', 'Next Stop'],
['hero', 'cta_book', 'Book Now'],
['hero', 'cta_gallery', 'Gallery'],
['hero', 'cta_tour_dates', 'Tour Dates'],
// home
['home', 'section_rates', 'Rates'],
['home', 'section_gallery', 'Gallery'],
['home', 'section_tour', 'Tour'],
['home', 'cta_full_rates', 'Full rates & services'],
['home', 'cta_full_schedule', 'Full schedule & availability'],
['home', 'cta_view_all_photos', 'View all photos'],
['home', 'reveal_hint', 'Tap to reveal'],
// about
['about', 'section_title', 'About'],
['about', 'section_details', 'Details'],
['about', 'heading_physical', 'Physical'],
['about', 'heading_appearance', 'Appearance & Identity'],
['about', 'heading_available_for', 'Available For'],
['about', 'heading_available_to', 'Available To'],
['about', 'heading_menu', 'Menu'],
['about', 'touring_currently_in', 'Currently in {city}, {state} — see where I\'m headed →'],
['about', 'touring_heading_soon', 'Heading to {city}, {state} soon — full schedule →'],
['about', 'link_see_schedule', 'see where I\'m headed →'],
['about', 'link_full_schedule', 'full schedule →'],
['about', 'stat_age', 'Age'],
['about', 'stat_height', 'Height'],
['about', 'stat_weight', 'Weight'],
['about', 'stat_body_type', 'Body Type'],
['about', 'stat_cup_size', 'Cup Size'],
['about', 'stat_bust', 'Bust'],
['about', 'stat_waist', 'Waist'],
['about', 'stat_hips', 'Hips'],
['about', 'stat_ethnicity', 'Ethnicity'],
['about', 'stat_hair', 'Hair'],
['about', 'stat_hair_length', 'Hair Length'],
['about', 'stat_eyes', 'Eyes'],
['about', 'stat_tattoos', 'Tattoos'],
['about', 'stat_piercings', 'Piercings'],
['about', 'stat_trans_status', 'Trans Status'],
['about', 'stat_sexual_role', 'Sexual Role'],
['about', 'stat_languages', 'Languages'],
['about', 'meta_title', 'About Quinn'],
['about', 'meta_description', 'Background, vibe, and what to expect.'],
// rates
['rates', 'section_title', 'Rates'],
['rates', 'subtitle', 'All prices in USD'],
['rates', 'label_addons', 'Add-Ons'],
['rates', 'label_fmty', 'Fly Me To You (FMTY)'],
['rates', 'label_online_services', 'Online Services'],
['rates', 'cta_tour_schedule', 'See tour schedule →'],
['rates', 'cta_how_to_book', 'How to book →'],
['rates', 'cta_contact', 'Contact Quinn →'],
['rates', 'meta_title', 'Rates — Quinn'],
['rates', 'meta_description', 'Service menu and rates.'],
// tour
['tour', 'section_title', 'Tour Schedule'],
['tour', 'subtitle', '2026 World Tour'],
['tour', 'note_1', 'Dates shift based on bookings — text early if a city interests you.'],
['tour', 'note_2', 'Deposits required for all touring bookings. Hotel provided as incall at each stop — book at standard rates.'],
['tour', 'note_3', 'For international cities, contact directly for availability and logistics.'],
['tour', 'home_base_label', 'Current Location'],
['tour', 'fmty_section_title', 'Fly Me To You'],
['tour', 'fmty_subtitle', 'Quinn comes to you — anywhere in the world'],
['tour', 'fmty_description_1', 'Fly Me To You means Quinn travels to your city — domestic or international. Flights and accommodation are on her; the rate is fully all-inclusive. Text to discuss your location and dates; a deposit is required to secure the trip.'],
['tour', 'fmty_description_2', 'FMTY is available anywhere in the world, not limited to current tour stops. West Coast and Las Vegas rates apply to CA and NV cities. North America covers all other domestic travel. International is anywhere outside North America.'],
['tour', 'section_destinations', 'Destinations'],
['tour', 'subtitle_destinations', 'Browse cities Quinn travels to'],
['tour', 'cta_all_destinations', 'View all destinations →'],
['tour', 'tier_west_coast', 'West Coast'],
['tour', 'tier_north_america', 'North America'],
['tour', 'tier_international', 'International'],
['tour', 'meta_title', 'Tour Schedule — Quinn'],
['tour', 'meta_description', 'Upcoming cities and dates.'],
// gallery
['gallery', 'section_title', 'Gallery'],
['gallery', 'meta_title', 'Gallery — Quinn'],
['gallery', 'meta_description', 'Photos and looks.'],
// contact
['contact', 'section_title', 'Contact'],
['contact', 'label_availability', 'Availability'],
['contact', 'label_payment', 'Payment'],
['contact', 'label_whatsapp', 'WhatsApp'],
['contact', 'meta_title', 'Contact — Quinn'],
['contact', 'meta_description', 'Get in touch.'],
// booking
['booking', 'section_title', 'Book an Appointment'],
['booking', 'subtitle', 'Four simple steps'],
['booking', 'section_contact', 'Contact'],
['booking', 'step_1_title', 'Text Quinn'],
['booking', 'step_1_body', 'Available 24/7 — typically booking about a week out, but same-day often works. Send a text with what you have in mind: date, time, how long, in or out. The more you share, the faster I can say yes.'],
['booking', 'step_2_title', 'Send Deposit'],
['booking', 'step_2_body', 'Once we agree on details, a deposit locks it in. I don\'t start getting ready until it\'s received — so earlier is better.'],
['booking', 'step_3_title', 'Get Confirmed'],
['booking', 'step_3_body', 'I confirm, share the location (incall), and you\'re all set. Simple, discreet, no drama.'],
['booking', 'step_4_title', 'Enjoy'],
['booking', 'step_4_body', 'Show up. Relax. I\'ll handle the rest.'],
['booking', 'meta_title', 'Booking — Quinn'],
['booking', 'meta_description', 'How to book with Quinn.'],
// links
['links', 'cta_book_now', 'Book Now'],
['links', 'cta_gallery', 'Gallery'],
['links', 'cta_rates', 'Rates'],
['links', 'cta_tour_dates', 'Tour Dates'],
['links', 'cta_about', 'About'],
['links', 'cta_whatsapp', 'WhatsApp'],
['links', 'cta_sms', 'Text / SMS'],
['links', 'cta_tryst', 'Tryst.link'],
['links', 'cta_onlyfans', 'OnlyFans'],
['links', 'cta_transfans', 'TransFans'],
['links', 'cta_fansly', 'Fansly'],
['links', 'cta_loyalfans', 'LoyalFans'],
['links', 'cta_fancentro', 'FanCentro'],
['links', 'cta_fantime', 'FanTime'],
['links', 'footer_brand', 'transquinnftw.com'],
['links', 'meta_title', 'Links — Quinn'],
['links', 'meta_description', 'All links.'],
// footer
['footer', 'label_contact', 'Contact'],
['footer', 'label_payment', 'Payment'],
['footer', 'label_social', 'Social'],
['footer', 'label_touring', 'Touring'],
['footer', 'label_specialties', 'Specialties'],
['footer', 'cta_send_message', 'Send a message →'],
['footer', 'cta_tour_schedule', 'Tour Schedule'],
['footer', 'cta_destinations', 'Destinations'],
['footer', 'cta_fmty', 'Fly Me To You'],
['footer', 'disclaimer_copyright', 'All content is the property of {name}. Unauthorized reproduction prohibited.'],
['footer', 'disclaimer_analytics', 'This site uses cookieless analytics — no personal data collected.'],
// destinations (index page)
['destinations', 'section_title', 'Destinations'],
['destinations', 'subtitle', 'Worldwide — Fly Me To You'],
['destinations', 'intro_part1', 'Quinn travels anywhere in the world. FMTY (Fly Me To You) means she handles flights and accommodation — all you arrange is your time. Browse the cities below, or'],
['destinations', 'intro_link_text', 'book directly'],
['destinations', 'intro_part2', "if your city isn't listed."],
['destinations', 'badge_on_tour', 'On Tour'],
['destinations', 'meta_title', 'Destinations — Quinn | Worldwide FMTY'],
['destinations', 'meta_description', 'Quinn is an upscale trans escort available worldwide via Fly Me To You. Browse cities and book your destination.'],
// destination (per-city pages)
['destination', 'callout_visiting', 'Quinn is visiting'],
['destination', 'cta_make_appointment', 'Make an Appointment'],
['destination', 'experiences_heading', 'What a session in {city} looks like'],
['destination', 'includes_label', "What's included"],
['destination', 'includes_flights', 'Round-trip international flights'],
['destination', 'includes_hotel', 'Hotel accommodation for the full stay'],
['destination', 'includes_time', 'Her complete time — no clock-watching'],
['destination', 'includes_logistics', 'All travel logistics handled entirely by Quinn'],
['destination', 'cta_text_quinn', 'Text Quinn'],
['destination', 'cta_whatsapp', 'WhatsApp'],
['destination', 'cta_view_rates', 'View all rates →'],
['destination', 'section_booking_title', 'Book Your Appointment'],
['destination', 'booking_text', 'Text directly to discuss your city, dates, and availability. A deposit is required to secure any FMTY trip. Quinn responds 24/7 — typically within a few hours.'],
['destination', 'cta_view_policies', 'View booking policies →'],
['destination', 'cta_see_all_rates', 'See all rates →'],
['destination', 'cta_see_tour', 'View tour schedule →'],
// specialties (index page)
['specialties', 'section_title', 'Specialties'],
['specialties', 'subtitle', 'Full Service Menu'],
['specialties', 'intro_part1', "Everything Quinn offers — from girlfriend experience to kink-friendly exploration. Each specialty page has details on what to expect and how to book."],
['specialties', 'intro_link_text', 'Book directly'],
['specialties', 'intro_part2', 'to discuss anything not listed.'],
['specialties', 'meta_title', 'Specialties — Quinn | San Francisco Trans Escort'],
['specialties', 'meta_description', "Browse Quinn's full menu of services — GFE, overnight sessions, kink-friendly experiences, and more. Upscale trans escort in San Francisco."],
// specialty (per-item pages)
['specialty', 'what_to_expect', 'What to expect'],
['specialty', 'book_title', 'Book This Experience'],
['specialty', 'book_body', "Text Quinn directly to discuss availability and details. Mention what you're interested in — she appreciates specificity."],
['specialty', 'cta_text_quinn', 'Text Quinn'],
['specialty', 'cta_whatsapp', 'WhatsApp'],
['specialty', 'cta_see_rates', 'See all rates →'],
['specialty', 'cta_booking_policies', 'Booking policies →'],
['specialty', 'more_in_prefix', 'More in'],
];
for (const [ns, key, value] of siteTextDefaults) {
textStmt.run(ns, key, value);
}
touchLastModified();
db.exec('COMMIT');
// Count records
const count = (table: string): number =>
(db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get() as { c: number }).c;
logger.info('Migration complete', {
rateSections: count('rate_sections'),
rateEntries: count('rate_entries'),
tourStops: count('tour_stops'),
galleryItems: count('gallery_items'),
policySections: count('policy_sections'),
policyItems: count('policy_items'),
etiquetteSections: count('etiquette_sections'),
etiquetteItems: count('etiquette_items'),
activityMenus: count('activity_menus'),
destinations: count('destinations'),
specialties: count('specialties'),
siteText: count('site_text'),
});
} catch (err) {
db.exec('ROLLBACK');
logger.error('Migration failed', { error: String(err) });
process.exit(1);
}
}
main();