| 1 | import pg from 'pg'
|
| 2 |
|
| 3 | const { Client } = pg
|
| 4 |
|
| 5 | const dbUrl = process.env.DATABASE_URL ?? 'postgres://user:password@localhost:5430/orbit_dev'
|
| 6 | const targetEmail = process.env.SEED_EMAIL ?? 'porcellis@eletrotupi.com'
|
| 7 | const DAYS = 35
|
| 8 |
|
| 9 | // Reproducible PRNG so the demo dataset is stable between runs.
|
| 10 | function mulberry32(seed) {
|
| 11 | return function () {
|
| 12 | seed |= 0
|
| 13 | seed = (seed + 0x6d2b79f5) | 0
|
| 14 | let t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
|
| 15 | t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
| 16 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
| 17 | }
|
| 18 | }
|
| 19 | const rand = mulberry32(20261019)
|
| 20 |
|
| 21 | const pick = (arr) => arr[Math.floor(rand() * arr.length)]
|
| 22 | const between = (min, max) => Math.floor(rand() * (max - min + 1)) + min
|
| 23 | const round1 = (n) => Math.round(n * 10) / 10
|
| 24 |
|
| 25 | function localDateStr(d) {
|
| 26 | return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
| 27 | }
|
| 28 |
|
| 29 | // ---- mood ----
|
| 30 | function pickMood() {
|
| 31 | const r = rand()
|
| 32 | if (r < 0.15) return 'GREAT'
|
| 33 | if (r < 0.5) return 'GOOD'
|
| 34 | if (r < 0.75) return 'NEUTRAL'
|
| 35 | if (r < 0.9) return 'SAD'
|
| 36 | return 'ANGRY'
|
| 37 | }
|
| 38 |
|
| 39 | const MOOD_LEVELS = {
|
| 40 | GREAT: { energy: [7, 9], stress: [1, 3], anxiety: [1, 3] },
|
| 41 | GOOD: { energy: [6, 8], stress: [2, 4], anxiety: [2, 4] },
|
| 42 | NEUTRAL: { energy: [4, 6], stress: [3, 5], anxiety: [3, 5] },
|
| 43 | SAD: { energy: [2, 4], stress: [4, 6], anxiety: [5, 7] },
|
| 44 | ANGRY: { energy: [4, 6], stress: [7, 9], anxiety: [6, 8] },
|
| 45 | }
|
| 46 |
|
| 47 | const MOOD_COMPONENTS = {
|
| 48 | GREAT: ['JOY', 'GRATITUDE', 'MOTIVATED', 'CALM'],
|
| 49 | GOOD: ['CALM', 'FOCUS', 'RELAXED', 'MOTIVATED'],
|
| 50 | NEUTRAL: ['CALM', 'TIREDNESS', 'FOCUS', 'RELAXED'],
|
| 51 | SAD: ['SADNESS', 'TIREDNESS', 'OVERWHELMED', 'GUILT', 'FEAR'],
|
| 52 | ANGRY: ['ANGER', 'FRUSTRATION', 'RESTLESS'],
|
| 53 | }
|
| 54 |
|
| 55 | const MOOD_ANNOTATIONS = {
|
| 56 | GREAT: ['Dia muito produtivo', 'Acordei bem disposto', 'Me senti leve e animado', null],
|
| 57 | GOOD: ['Dia tranquilo', 'Boa noite de sono', 'Trabalho rendeu bem', null],
|
| 58 | NEUTRAL: ['Nada de especial hoje', 'Rotina normal', 'Cansado, mas ok', null],
|
| 59 | SAD: ['Noite mal dormida', 'Me senti para baixo', 'Sem energia hoje', null],
|
| 60 | ANGRY: ['Estresse no trabalho', 'Briga em casa', 'Trânsito me irritou', null],
|
| 61 | }
|
| 62 |
|
| 63 | const INTENSITIES = ['LIGHT', 'MODERATE', 'HIGH']
|
| 64 |
|
| 65 | function componentsFor(mood) {
|
| 66 | const pool = MOOD_COMPONENTS[mood]
|
| 67 | const count = between(1, Math.min(3, pool.length))
|
| 68 | const chosen = new Set()
|
| 69 | while (chosen.size < count) chosen.add(pick(pool))
|
| 70 |
|
| 71 | return [...chosen].map((component) => {
|
| 72 | let intensity = pick(INTENSITIES)
|
| 73 | if (['ANGER', 'FEAR', 'OVERWHELMED', 'FRUSTRATION'].includes(component) && intensity === 'LIGHT') {
|
| 74 | intensity = 'MODERATE'
|
| 75 | }
|
| 76 | if (['CALM', 'RELAXED'].includes(component) && intensity === 'HIGH') {
|
| 77 | intensity = 'MODERATE'
|
| 78 | }
|
| 79 | return { component, intensity }
|
| 80 | })
|
| 81 | }
|
| 82 |
|
| 83 | // ---- sleep ----
|
| 84 | const SLEEP_NOTES = ['Boa noite de sono', 'Acordei de madrugada', 'Demorei para pegar no sono', 'Sono leve', null]
|
| 85 |
|
| 86 | // ---- triggers ----
|
| 87 | const TRIGGER_POOL = [
|
| 88 | { category: 'WORK', comment: 'Reunião estressante no trabalho' },
|
| 89 | { category: 'WORK', comment: 'Prazo apertado no projeto' },
|
| 90 | { category: 'FAMILY', comment: 'Discussão em família' },
|
| 91 | { category: 'PHYSICAL', comment: 'Noite mal dormida' },
|
| 92 | { category: 'PHYSICAL', comment: 'Dor de cabeça forte' },
|
| 93 | { category: 'INTERNAL', comment: 'Pensamentos ansiosos' },
|
| 94 | { category: 'INTERNAL', comment: 'Sentimento de culpa' },
|
| 95 | { category: 'SOCIAL', comment: 'Encontro com amigos' },
|
| 96 | { category: 'SOCIAL', comment: 'Evento social no fim de semana' },
|
| 97 | { category: 'HEALTH', comment: 'Consulta de rotina' },
|
| 98 | { category: 'THERAPY', comment: 'Sessão de terapia' },
|
| 99 | { category: 'FAMILY', comment: 'Visita de parentes' },
|
| 100 | ]
|
| 101 |
|
| 102 | // ---- medicine ----
|
| 103 | const REGIMENS = [
|
| 104 | { name: 'Lítio', dosage: '300 mg', periodicity: 'DAILY', scheduledAt: ['20:00'] },
|
| 105 | { name: 'Lamotrigina', dosage: '100 mg', periodicity: 'DAILY', scheduledAt: ['08:00'] },
|
| 106 | ]
|
| 107 |
|
| 108 | // ---- activities ----
|
| 109 | const ACTIVITY_POOL = [
|
| 110 | { type: 'WALK', duration: 30 },
|
| 111 | { type: 'WALK', duration: 45 },
|
| 112 | { type: 'MEDITATION', duration: 15 },
|
| 113 | { type: 'GYM', duration: 60 },
|
| 114 | { type: 'YOGA', duration: 45 },
|
| 115 | { type: 'CREATIVE', duration: 60 },
|
| 116 | { type: 'SOCIAL', duration: 90 },
|
| 117 | ]
|
| 118 |
|
| 119 | async function main() {
|
| 120 | const db = new Client({ connectionString: dbUrl })
|
| 121 | await db.connect()
|
| 122 |
|
| 123 | try {
|
| 124 | const { rows } = await db.query(
|
| 125 | 'SELECT id, first_name, last_name FROM users WHERE email = $1',
|
| 126 | [targetEmail],
|
| 127 | )
|
| 128 | if (rows.length === 0) {
|
| 129 | console.error(`User ${targetEmail} not found`)
|
| 130 | process.exitCode = 1
|
| 131 | return
|
| 132 | }
|
| 133 |
|
| 134 | const user = rows[0]
|
| 135 | const userId = user.id
|
| 136 |
|
| 137 | // Wipe existing demo data, children first (FK-safe order).
|
| 138 | await db.query('DELETE FROM care_actions WHERE user_id = $1', [userId])
|
| 139 | await db.query('DELETE FROM medicine_regimens WHERE user_id = $1', [userId])
|
| 140 | await db.query('DELETE FROM triggers WHERE user_id = $1', [userId])
|
| 141 | await db.query('DELETE FROM sleep_records WHERE user_id = $1', [userId])
|
| 142 | await db.query('DELETE FROM moods WHERE user_id = $1', [userId])
|
| 143 | await db.query('DELETE FROM insights WHERE user_id = $1', [userId])
|
| 144 |
|
| 145 | const now = new Date()
|
| 146 | const dayAt = (offset) => {
|
| 147 | const d = new Date(now)
|
| 148 | d.setDate(now.getDate() - offset)
|
| 149 | return d
|
| 150 | }
|
| 151 |
|
| 152 | const counts = { moods: 0, sleep: 0, triggers: 0, medicine: 0, activities: 0 }
|
| 153 |
|
| 154 | // moods
|
| 155 | for (let d = DAYS; d >= 0; d--) {
|
| 156 | const r = rand()
|
| 157 | const count = r < 0.05 ? 0 : r < 0.5 ? 1 : r < 0.85 ? 2 : 3
|
| 158 |
|
| 159 | for (let i = 0; i < count; i++) {
|
| 160 | const mood = pickMood()
|
| 161 | const levels = MOOD_LEVELS[mood]
|
| 162 |
|
| 163 | const moment = new Date(dayAt(d))
|
| 164 | moment.setHours(pick([9, 12, 15, 18, 21]) + between(0, 2), between(0, 59), 0, 0)
|
| 165 |
|
| 166 | const res = await db.query(
|
| 167 | `INSERT INTO moods
|
| 168 | (user_id, selected_mood, energy_level, stress_level, anxiety_level, annotation, moment)
|
| 169 | VALUES ($1, $2, $3, $4, $5, $6, $7)
|
| 170 | RETURNING id`,
|
| 171 | [
|
| 172 | userId,
|
| 173 | mood,
|
| 174 | between(...levels.energy),
|
| 175 | between(...levels.stress),
|
| 176 | between(...levels.anxiety),
|
| 177 | pick(MOOD_ANNOTATIONS[mood]),
|
| 178 | moment,
|
| 179 | ],
|
| 180 | )
|
| 181 |
|
| 182 | for (const c of componentsFor(mood)) {
|
| 183 | await db.query(
|
| 184 | 'INSERT INTO mood_components (mood_id, component, intensity) VALUES ($1, $2, $3)',
|
| 185 | [res.rows[0].id, c.component, c.intensity],
|
| 186 | )
|
| 187 | }
|
| 188 | counts.moods++
|
| 189 | }
|
| 190 | }
|
| 191 |
|
| 192 | // sleep records — one per night
|
| 193 | for (let d = DAYS; d >= 0; d--) {
|
| 194 | const day = dayAt(d)
|
| 195 | const average = round1(5.5 + rand() * 3)
|
| 196 | const note = pick(SLEEP_NOTES)
|
| 197 | await db.query(
|
| 198 | 'INSERT INTO sleep_records (user_id, average, date, annotations, updated_at) VALUES ($1, $2, $3, $4, NOW())',
|
| 199 | [userId, average, localDateStr(day), note],
|
| 200 | )
|
| 201 | counts.sleep++
|
| 202 | }
|
| 203 |
|
| 204 | // triggers
|
| 205 | for (let i = 0; i < TRIGGER_POOL.length; i++) {
|
| 206 | const t = TRIGGER_POOL[i]
|
| 207 | const moment = new Date(dayAt(between(0, DAYS)))
|
| 208 | moment.setHours(between(8, 21), between(0, 59), 0, 0)
|
| 209 | await db.query(
|
| 210 | 'INSERT INTO triggers (user_id, category, moment, comment, updated_at) VALUES ($1, $2, $3, $4, NOW())',
|
| 211 | [userId, t.category, moment, t.comment],
|
| 212 | )
|
| 213 | counts.triggers++
|
| 214 | }
|
| 215 |
|
| 216 | // medicine regimens + logs
|
| 217 | for (const r of REGIMENS) {
|
| 218 | const reg = await db.query(
|
| 219 | `INSERT INTO medicine_regimens (user_id, name, dosage, periodicity, scheduled_at, active, updated_at)
|
| 220 | VALUES ($1, $2, $3, $4, $5, true, NOW())
|
| 221 | RETURNING id`,
|
| 222 | [userId, r.name, r.dosage, r.periodicity, r.scheduledAt],
|
| 223 | )
|
| 224 | const regimenId = reg.rows[0].id
|
| 225 |
|
| 226 | for (let d = DAYS; d >= 0; d--) {
|
| 227 | if (rand() < 0.25) continue
|
| 228 |
|
| 229 | const moment = new Date(dayAt(d))
|
| 230 | const [h, m] = pick(r.scheduledAt).split(':').map(Number)
|
| 231 | moment.setHours(h, m, 0, 0)
|
| 232 |
|
| 233 | const ca = await db.query(
|
| 234 | "INSERT INTO care_actions (user_id, type, moment, updated_at) VALUES ($1, 'MEDICINE', $2, NOW()) RETURNING id",
|
| 235 | [userId, moment],
|
| 236 | )
|
| 237 | await db.query(
|
| 238 | 'INSERT INTO medicine_logs (care_action_id, regimen_id, taken_at) VALUES ($1, $2, $3)',
|
| 239 | [ca.rows[0].id, regimenId, moment],
|
| 240 | )
|
| 241 | counts.medicine++
|
| 242 | }
|
| 243 | }
|
| 244 |
|
| 245 | // activities
|
| 246 | for (let i = 0; i < 10; i++) {
|
| 247 | const a = pick(ACTIVITY_POOL)
|
| 248 | const moment = new Date(dayAt(between(0, DAYS)))
|
| 249 | moment.setHours(between(7, 20), between(0, 59), 0, 0)
|
| 250 |
|
| 251 | const ca = await db.query(
|
| 252 | "INSERT INTO care_actions (user_id, type, moment, updated_at) VALUES ($1, 'ACTIVITY', $2, NOW()) RETURNING id",
|
| 253 | [userId, moment],
|
| 254 | )
|
| 255 | await db.query(
|
| 256 | 'INSERT INTO activities (care_action_id, type, duration) VALUES ($1, $2, $3)',
|
| 257 | [ca.rows[0].id, a.type, a.duration],
|
| 258 | )
|
| 259 | counts.activities++
|
| 260 | }
|
| 261 |
|
| 262 | const name = [user.first_name, user.last_name].filter(Boolean).join(' ')
|
| 263 | console.log(`Seeded demo data for ${targetEmail} (id ${user.id}, ${name})`)
|
| 264 | console.log(counts)
|
| 265 | } finally {
|
| 266 | await db.end()
|
| 267 | }
|
| 268 | }
|
| 269 |
|
| 270 | main().catch((err) => {
|
| 271 | console.error(err)
|
| 272 | process.exit(1)
|
| 273 | })
|