fix(standards): make the error-shape ratchet see require(); convert the 4 files it exposed
no-restricted-imports never fires on require() calls, so the ratchet was toothless — switched to no-restricted-syntax on the require literal. It immediately flagged four residual files whose multi-line res.status chains the conversion greps had missed: user.cts (four err.message-at-500 leaks — same class as QA-B13-02), calendar.cts, export.cts, and payments.cts's DUPLICATE_SUSPECTED body (now a hand-built literal of the standard shape, import dropped). Lint enforces the ratchet for real now; 252/252. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bdff5ad932
commit
a1b7011149
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
|
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
|
||||||
import type { Req, Res, Next } from '../types/http';
|
import type { Req, Res, Next } from '../types/http';
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
const { ValidationError } = require('../utils/apiError.cts');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
@ -22,16 +22,10 @@ router.get('/', (req: Req, res: Res) => {
|
||||||
let where, whereParams, label;
|
let where, whereParams, label;
|
||||||
if (from != null || to != null) {
|
if (from != null || to != null) {
|
||||||
if (!isDate(from) || !isDate(to)) {
|
if (!isDate(from) || !isDate(to)) {
|
||||||
return res
|
throw ValidationError('from and to must both be YYYY-MM-DD dates', 'from');
|
||||||
.status(400)
|
|
||||||
.json(
|
|
||||||
standardizeError('from and to must both be YYYY-MM-DD dates', 'VALIDATION_ERROR', 'from'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (from > to) {
|
if (from > to) {
|
||||||
return res
|
throw ValidationError('from must be on or before to', 'from');
|
||||||
.status(400)
|
|
||||||
.json(standardizeError('from must be on or before to', 'VALIDATION_ERROR', 'from'));
|
|
||||||
}
|
}
|
||||||
where = 'p.paid_date BETWEEN ? AND ?';
|
where = 'p.paid_date BETWEEN ? AND ?';
|
||||||
whereParams = [from, to];
|
whereParams = [from, to];
|
||||||
|
|
@ -39,15 +33,7 @@ router.get('/', (req: Req, res: Res) => {
|
||||||
} else {
|
} else {
|
||||||
const year = parseInt(req.query.year || new Date().getFullYear(), 10);
|
const year = parseInt(req.query.year || new Date().getFullYear(), 10);
|
||||||
if (isNaN(year) || year < 2000 || year > 2100) {
|
if (isNaN(year) || year < 2000 || year > 2100) {
|
||||||
return res
|
throw ValidationError('year must be a 4-digit integer between 2000 and 2100', 'year');
|
||||||
.status(400)
|
|
||||||
.json(
|
|
||||||
standardizeError(
|
|
||||||
'year must be a 4-digit integer between 2000 and 2100',
|
|
||||||
'VALIDATION_ERROR',
|
|
||||||
'year',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
where = "strftime('%Y', p.paid_date) = ?";
|
where = "strftime('%Y', p.paid_date) = ?";
|
||||||
whereParams = [String(year)];
|
whereParams = [String(year)];
|
||||||
|
|
@ -342,11 +328,19 @@ function buildUserDbExportFile(userId) {
|
||||||
|
|
||||||
router.get('/user-db', (req: Req, res: Res) => {
|
router.get('/user-db', (req: Req, res: Res) => {
|
||||||
const file = buildUserDbExportFile(req.user.id);
|
const file = buildUserDbExportFile(req.user.id);
|
||||||
res.download(file, 'bill-tracker-user-export.sqlite', () => {
|
// root-option form: send >= 2026-07 rejects bare absolute paths (4a dep sweep)
|
||||||
try {
|
res.download(
|
||||||
fs.unlinkSync(file);
|
path.basename(file),
|
||||||
} catch {}
|
'bill-tracker-user-export.sqlite',
|
||||||
});
|
{
|
||||||
|
root: path.dirname(file),
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(file);
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Full portable JSON export of the user's data (same assembly as SQLite/Excel).
|
// Full portable JSON export of the user's data (same assembly as SQLite/Excel).
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
|
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
|
||||||
import type { Req, Res, Next } from '../types/http';
|
import type { Req, Res, Next } from '../types/http';
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
|
||||||
const {
|
const {
|
||||||
ApiError,
|
ApiError,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
|
|
@ -269,12 +268,14 @@ router.post('/', (req: Req, res: Res) => {
|
||||||
)
|
)
|
||||||
.get(bill.id, payment.paid_date, payment.amount);
|
.get(bill.id, payment.paid_date, payment.amount);
|
||||||
if (existingDuplicate) {
|
if (existingDuplicate) {
|
||||||
|
// Deliberate hand-rolled body (documented exception): carries the
|
||||||
|
// `existing` payment the client's "add anyway?" flow renders —
|
||||||
|
// formatError has no extras channel. Shape matches the standard envelope.
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
...standardizeError(
|
error: 'DUPLICATE_SUSPECTED',
|
||||||
'A matching payment already exists for this bill, date, and amount',
|
message: 'A matching payment already exists for this bill, date, and amount',
|
||||||
'DUPLICATE_SUSPECTED',
|
code: 'DUPLICATE_SUSPECTED',
|
||||||
'amount',
|
field: 'amount',
|
||||||
),
|
|
||||||
existing: serializePayment(existingDuplicate),
|
existing: serializePayment(existingDuplicate),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-export-richer-${process.pid}
|
||||||
process.env.DB_PATH = dbPath;
|
process.env.DB_PATH = dbPath;
|
||||||
|
|
||||||
const { getDb, closeDb } = require('../db/database.cts');
|
const { getDb, closeDb } = require('../db/database.cts');
|
||||||
|
const { formatError } = require('../utils/apiError.cts');
|
||||||
const exportRouter = require('../routes/export.cts');
|
const exportRouter = require('../routes/export.cts');
|
||||||
const { getUserExportData } = exportRouter;
|
const { getUserExportData } = exportRouter;
|
||||||
|
|
||||||
|
|
@ -39,7 +40,13 @@ function callExport(query) {
|
||||||
resolve({ status: this.statusCode, headers, body });
|
resolve({ status: this.statusCode, headers, body });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
handler(req, res);
|
// Routes follow the throw-pattern: mirror the app's terminal error handler
|
||||||
|
// so thrown ApiErrors resolve to the same wire shape.
|
||||||
|
try {
|
||||||
|
handler(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
resolve({ status: err.status || 500, headers, json: formatError(err) });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue