Skip to content

fix(email): create the email_logs table the service has been writing to - #1704

Open
MOHITKOURAV01 wants to merge 1 commit into
AnthropicBots:mainfrom
MOHITKOURAV01:fix/1699-email-logs-migration
Open

fix(email): create the email_logs table the service has been writing to#1704
MOHITKOURAV01 wants to merge 1 commit into
AnthropicBots:mainfrom
MOHITKOURAV01:fix/1699-email-logs-migration

Conversation

@MOHITKOURAV01

@MOHITKOURAV01 MOHITKOURAV01 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes #1699

What was wrong

backend/services/emailService.js reads from and writes to email_logs. Nothing in migrations/ creates it.

$ grep -rn "email_logs" migrations backend --include='*.sql' --include='*.js' | grep -v node_modules
backend/services/emailService.js:35:  `INSERT INTO email_logs (recipient, subject, order_id, status, channel, error) ...
backend/services/emailService.js:58:  ... FROM email_logs ORDER BY sent_at DESC LIMIT ?

Both failures were caught and thrown away:

await db.query(`INSERT INTO email_logs (...) ...`)
    .catch(() => {}); // Table created dynamically if needed

Nothing creates it dynamically. MySQL answered ER_NO_SUCH_TABLE on every insert, the .catch discarded it, and recordEmailLog returned as if it had succeeded. getEmailLogs caught the same error on the SELECT and fell through to emailLogBuffer — a 100-entry array in module scope.

So the "audit trail" was: capped at 100, empty after every restart or deploy, and different per instance in a multi-instance deployment. There is no record anywhere of a failed delivery once a process recycles, which is exactly what an operator needs when a customer says their confirmation never arrived.

What this adds

migrations/0052_email_logs.sql — the table, shaped by what the service actually issues rather than invented:

  • sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMPgetEmailLogs selects and orders on it; recordEmailLog never inserts it. Without a default every row sorts as NULL and "recent logs" means nothing.
  • order_id CHAR(36) NULL, no foreign keyrecordEmailLog passes null whenever the caller has no order, and a log entry has to outlive the thing it describes. An order erased under a data-deletion request must not take the record of what was mailed about it along with it, which is exactly what ON DELETE CASCADE would do. CHAR(36) matches orders.id.
  • status / channel as VARCHAR, not ENUM — the service already writes four statuses and two channels, and a new transport should not need a migration before it can log that it ran.
  • error TEXT — SMTP failures are routinely longer than 255 characters.
  • Three indexessent_at for the only query the service makes today (ORDER BY sent_at DESC LIMIT ? is a filesort over the whole table without it, and this table only grows), plus order_id and (status, sent_at) for the two questions an operator actually arrives with.

It takes 0052 rather than 0050: three migrations already collide on 0049, and resolving that needs 0050 and 0051 (#1700). Taking a number those two do not want keeps the two changes independent.

emailService.js stops hiding the failures. The write path reports what it could not persist and marks the entry persisted: false; the read path says when it is serving the memory buffer instead of the table; and an empty result set is treated as an answer rather than a failure — the old if (rows && rows.length > 0) fall-through made a working database look like a broken one the moment it had nothing to show, and hid the fact that the buffer was all anyone had ever been reading.

Guard

backend/tests/emailLogsSchema.test.js, 27 cases. Nothing in the repo runs migrations or touches MySQL — check:syntax parses, check:boot mounts, check:modules requires — and the suite that shipped with the feature asserted on the fallback buffer, which works fine with no table at all. So the check is static: parse the INSERT and SELECT out of the service, parse the CREATE TABLE body out of the migrations, and assert the two describe the same columns.

It also pins the migration is the table's only owner (migrations/README.md: "A table has exactly one owning migration" — a second CREATE TABLE IF NOT EXISTS is skipped silently), that the filename matches the runner's NNNN_name.sql pattern, and that the swallowing patterns cannot come back.

Verification

$ backend/node_modules/.bin/jest --config backend/jest.config.js tests/emailLogsSchema.test.js tests/emailService.test.js
Tests:       32 passed, 32 total

Confirmed the guard is real by removing the migration and reverting the service, then re-running the new test:

✕ some migration creates the table
✕ exactly one migration creates it
✕ the table declares recipient / subject / order_id / status / channel / error / id / sent_at
✕ order_id is nullable
✕ error is wide enough for what a transport throws
… 20 failures

The existing tests/emailService.test.js still passes unchanged — it exercises the buffer fallback, which this keeps, now with a line saying it is being used.


CI note — merge #1701 first

The Syntax check job fails on this branch, and it is not this change:

❌ 1 of 654 JavaScript file(s) failed to parse:
  frontend/scripts/shop.js:2499
      Unexpected end of input

frontend/scripts/shop.js is unparsable on main — the responsive refactor duplicated and interleaved its initialization block. Every open PR against this repository inherits it, and because check:syntax is the first CI job and the other two are gated on it, Backend tests and Server boots are skipped rather than run. That is why this PR shows no test result.

#1701 fixes it. Once that merges, this branch picks the fix up from main with no rebase needed — the two touch no file in common. All gates pass locally on this branch's changes:

$ npm run check:boot     ✅
$ npm run check:modules  ✅
$ npm run check:assets   ✅
$ npm run check:a11y     ✅
$ npm run check:sitemap  ✅

and I verified the whole set merges cleanly by merging all five of these branches together locally: no conflicts, check:syntax green at 659 files, and the full Jest suite at 2976 passing.

The Vercel check fails on every PR in this repository with "Authorization required to deploy" against the bhuvanshs-projects team, unrelated to any change.

emailService has read from and written to email_logs since the order
confirmation feature landed. No migration ever created it. Both failures were
caught and discarded -- the INSERT behind a bare .catch(() => {}) with a comment
claiming the table was created dynamically, the SELECT behind a catch that fell
through to a 100-entry array in module scope -- so every write has been dropped
and the admin log view has been serving a per-process buffer that empties on
restart.

Add 0052_email_logs.sql with the columns the service actually reads and writes.
sent_at carries a default because getEmailLogs orders on it and recordEmailLog
never inserts it; without one every row sorts as NULL. order_id is nullable and
carries no foreign key: recordEmailLog passes null when there is no order, and a
log entry has to outlive the thing it describes, which ON DELETE CASCADE would
prevent.

Stop hiding the failures. The write path reports what it could not persist, the
read path says when it is serving the memory buffer instead of the table, and an
empty table is treated as an answer rather than as a broken database -- that
fall-through made a working install look like a failed one the moment it had
nothing to show, and hid the fact that the buffer was all anyone was reading.
@hydra-maintainer

Copy link
Copy Markdown

🔍 Quality Gate Report

✅ All quality gates passed!

Status Check Details
Linked Issue PR description references a closing issue ✅

@hydra-maintainer

Copy link
Copy Markdown

🤖 AI Code Review

🔴 Score: 50/100 | comment

AI review unavailable at this time.


Automated AI review — a human maintainer will also review.

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Bhuvansh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@hydra-maintainer

Copy link
Copy Markdown

💡 Suggested reviewers based on relevant file history: @Aditya8369, @Pcmhacker-hero

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] email_logs table has no migration — every email audit record is silently dropped and the admin log view empties on restart

1 participant