Developer Cheat Sheet - jra3/mulm GitHub Wiki
Quick reference for common development tasks, commands, and code patterns in the Mulm project.
npm run dev # Start dev server with hot reload
npm run build # Build TypeScript + CSS
npm start # Start production build
npm run lint # Check code style
npm run lint:fix # Auto-fix linting issuesnpm test # Run all tests
npm test -- src/__tests__/mytest.test.ts # Run specific test
npm run test:watch # Watch modesqlite3 db/database.db # Open DB CLI (development)
sqlite3 db/database.db ".schema" # Show schema
sqlite3 db/database.db "SELECT * FROM members;" # Query
# For production, use: ssh BAP "sqlite3 /mnt/basny-data/app/database/database.db"ssh BAP "cd /opt/basny && git pull && sudo docker-compose -f docker-compose.prod.yml up -d --build"src/
├── routes/ # HTTP handlers → res.render() or res.json()
├── db/ # Database queries → query(), insertOne(), updateOne()
├── forms/ # Zod schemas → validate user input
├── utils/ # Helpers → r2-client, logger, image-processor
├── views/ # Pug templates → HTML rendering
└── __tests__/ # Tests → *.test.ts
// 1. Create handler in src/routes/myfeature.ts
import { MulmRequest } from '@/sessions';
import { Response } from 'express';
export const showMyFeature = async (req: MulmRequest, res: Response) => {
const { viewer } = req; // Current user (or undefined)
res.render('myfeature', {
title: 'My Feature',
viewer,
});
};
// 2. Register in src/index.ts
import { showMyFeature } from './routes/myfeature';
app.get('/myfeature', showMyFeature);
// 3. Create view in src/views/myfeature.pug
extends layout
block content
h1 My Feature// Read query
import { query } from '@/db/conn';
const members = await query<MemberRecord>(
'SELECT * FROM members WHERE is_admin = ?',
[1]
);
// Insert
import { insertOne } from '@/db/conn';
await insertOne('members', {
contact_email: '[email protected]',
display_name: 'User Name',
is_admin: 0,
});
// Update
import { updateOne } from '@/db/conn';
await updateOne(
'members',
{ id: memberId }, // WHERE clause
{ display_name: 'New Name' } // SET clause
);
// Transaction
import { withTransaction } from '@/db/conn';
await withTransaction(async (db) => {
await db.run('INSERT INTO members ...');
await db.run('UPDATE submissions ...');
// Atomic - both succeed or both rollback
});// 1. Define schema in src/forms/myform.ts
import { z } from 'zod';
export const myFormSchema = z.object({
name: z.string().min(2, 'Name too short'),
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be 18+'),
});
// 2. Validate in route handler
import { myFormSchema } from '@/forms/myform';
const result = myFormSchema.safeParse(req.body);
if (!result.success) {
// Show errors
const errors = new Map<string, string>();
result.error.issues.forEach(issue => {
errors.set(issue.path[0].toString(), issue.message);
});
res.render('myform', { errors, form: req.body });
return;
}
// result.data is typed and validated
const { name, email, age } = result.data;import { MulmRequest } from '@/sessions';
export const myRoute = async (req: MulmRequest, res: Response) => {
const { viewer } = req; // MemberRecord | undefined
// Check if logged in
if (!viewer) {
return res.redirect('/auth/signin');
}
// Check if admin
if (!viewer.is_admin) {
return res.status(403).send('Forbidden');
}
// Use viewer data
console.log(`User ${viewer.display_name} (ID: ${viewer.id})`);
};import { describe, test, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';
import { setupTestDatabase, createTestMembers } from './testDbHelper.helper';
describe('My Feature', () => {
let testDb;
beforeEach(async () => {
testDb = await setupTestDatabase();
});
afterEach(async () => {
await testDb.cleanup();
});
test('should do something', async () => {
// Arrange
const [member] = await createTestMembers(1);
// Act
const result = await myFunction(member.id);
// Assert
assert.strictEqual(result, expected);
});
});-- db/migrations/016-add-my-field.sql
-- Up
ALTER TABLE submissions ADD COLUMN my_field TEXT DEFAULT NULL;
CREATE INDEX idx_submissions_my_field ON submissions(my_field);
-- Down
DROP INDEX IF EXISTS idx_submissions_my_field;
-- Note: SQLite doesn't support DROP COLUMN//- Conditional rendering
if viewer
p Welcome, #{viewer.display_name}!
else
a(href="/auth/signin") Sign In
//- Loop
each submission in submissions
div.submission-card
h3= submission.species_common_name
p Points: #{submission.points}
//- HTMX dynamic content
button(
hx-post=`/admin/submissions/${submission.id}/approve`
hx-target="#submission-list"
hx-swap="outerHTML"
) Approve
//- Long Tailwind classes
div(
class="bg-gradient-to-r from-yellow-50 to-amber-50" +
" rounded-lg shadow-lg p-6"
)
//- Include mixin
include mixins/imageUpload
+imageUploadSection()NODE_ENV=development # Auto-set by npm run devNODE_ENV=test # Auto-set by npm test
# Silences logger, skips email sendingNODE_ENV=production # Set in docker-compose.prod.ymlimport { setupTestDatabase, createTestMembers } from './testDbHelper.helper';
const testDb = await setupTestDatabase(); // Fresh DB with migrations
const members = await createTestMembers(3); // 3 test members
await testDb.cleanup(); // Close connection// Get single record
const member = await query<MemberRecord>(
'SELECT * FROM members WHERE id = ?',
[id]
).then(rows => rows[0] || null);
// Get multiple records
const submissions = await query<Submission>(
'SELECT * FROM submissions WHERE member_id = ? ORDER BY created_on DESC',
[memberId]
);
// Count
const count = await query<{ count: number }>(
'SELECT COUNT(*) as count FROM members'
).then(rows => rows[0].count);git checkout main
git pull upstream main
git checkout -b feature/my-featuregit add .
git commit -m "feat(scope): description"
git push origin feature/my-featuregit checkout main
git pull upstream main
git checkout feature/my-feature
git rebase main
git push origin feature/my-feature --force-with-lease// Add breakpoints in VS Code
// Press F5 to start debugging
// Or add console.log (removed by linter if you forget)
console.log('Debug:', variable);# In test
test('debug db', async () => {
const all = await db.all('SELECT * FROM members');
console.log(all);
});
# In production
ssh BAP "sqlite3 /mnt/basny-data/app/database/database.db"
sqlite> SELECT * FROM submissions WHERE id = 123;# curl
curl -v https://bap.basny.org/api/species/search?q=guppy
# or browser DevTools Network tabexport const myRoute = async (req: MulmRequest, res: Response) => {
if (!req.viewer) {
return res.redirect('/auth/signin');
}
// ... authenticated route logic
};import { requireAdmin } from './routes/admin';
router.use('/admin/*', requireAdmin);
// Or inline:
export const myRoute = async (req: MulmRequest, res: Response) => {
if (!req.viewer?.is_admin) {
return res.status(403).send('Forbidden');
}
// ... admin logic
};const submission = await getSubmission(id);
// Parse JSON fields
const foods = JSON.parse(submission.foods) as string[];
const images = JSON.parse(submission.images || '[]') as ImageMetadata[];// Return full page
res.render('admin/queue', { submissions });
// Return partial (HTMX target)
res.render('admin/singleMemberRow', { member });import { query } from '@/db/conn'; // src/db/conn.ts
import { MulmRequest } from '@/sessions'; // src/sessions.ts
import config from '@/config.json'; // src/config.json
import { logger } from '@/utils/logger'; // src/utils/logger.tsConfigured in: tsconfig.json → paths
Bookmark this page for quick access to common patterns!