sql
Technical notes on web development, DevOps, and AI integration.
29 articles
- 09:17backend
Moving a Category Between Modules Is a Two-Part Job
One UPDATE sounds finished. If the importer map keeps the old value, the next import quietly resurrects stale data.
TL;DR: Moving the souvenir category to tourism required more than a database update. The migration uses a guarded WHERE clause to stay idempotent and reversible, while the importer map was updated in the same commit to prevent the old value from resurrecting. Wrapping it in a transaction would not help since MySQL auto-commits DDL.
#mysql#database-migration#golang - 07:30database
A Cosmetic Request That Forced a Data Contract Overhaul
Lowercasing an enum looks trivial; the values are compared literally in scope enforcement, so DDL, rows, constants, spec, and label maps move together.
TL;DR: A simple request to lowercase bidang values became a contract change since backend scopes match strings exactly and fail closed. Migration 000059 updated three database columns, backend constants, API specs and frontend types, normalizing rows while keeping ALL uppercase as the superadmin sentinel. The separate categories module was left untouched and a down migration was added for a clean rollback.
#mysql#go#migration - 07:27testing
When the Migration Table Lies in Bidang Integration Tests
Green suite, stale schema: schema_migrations only logs executed files, a leftover Docker volume quietly carries the old schema.
TL;DR: Tests stayed green after adding the bidang column because a stale MySQL volume kept the old schema while the migration version looked current. With no new files to run, the migrator did nothing and left the mismatch invisible. Now volumes are wiped before integration runs so migrations rebuild a fresh schema and six bidang scope scenarios are reliably verified.
#go#mysql#testing - 06:27backend
Setting Another User's Bidang: A Privileged Action
Only a superadmin may set a user bidang; the ALL default keeps 19 legacy users through the migration unharmed.
TL;DR: The schema change was simple, but deciding who can assign another user's scope was the real challenge. A central resolveBidang function lets only superadmins set it, returning 403 if others try while still allowing edits to other fields. Defaulting to ALL keeps existing users unlocked, with strict validation and eight tests locking the behavior.
#go#mysql#security - 06:27backend
Announcement Scope: Writing a Value vs Mutating a Row
Two authorization functions for per-division announcements: resolveBidang for new values, canManage for existing rows, NULL as global.
TL;DR: Portal now scopes announcements by division with two separate checks. resolveBidang governs creation, letting ALL admins write global NULL or any bidang while scoped writers are locked to their own. canManage governs existing rows, so only ALL can touch global content and labeled rows are owner-only, enforced in the service layer before any mutation.
#go#mysql#security - 06:27backend
Extensible Entity Attributes: a TEXT Column Holding JSON
Adding a TEXT attributes column for per-module dynamic attributes, with validation moved into the application.
TL;DR: Instead of adding nullable columns for each module, we added a single TEXT attributes field to handle flexible data. Since MySQL TEXT doesn't validate JSON, the app enforces rules like paired labels and no duplicates before saving. It's a lightweight escape hatch that avoids schema churn and only gets promoted to a real column when filtering is needed.
#go#mysql#json - 05:37backend
COALESCE at the API Door: Keep the ALL Sentinel in the Query
A super-admin without a bidang returns NULL from the database. One COALESCE in the login query keeps the API contract total and the admin badge correct.
TL;DR: The badge broke for super-admins because their bidang is NULL and old tokens don't even have the field. Rather than patching each Go handler, the team used COALESCE(bidang, 'ALL') in SQL to guarantee a valid string everywhere. TypeScript keeps bidang optional for legacy sessions, and React only renders the badge when the value isn't ALL.
#mysql#golang#typescript - 05:05backend
Bidang RBAC Enforcement: The Middleware Gate and IN Filters
Enforcing the bidang scope in Go: middleware at the gate, IN filters in repositories, and subtests that read like the security policy.
TL;DR: Middleware at the gate verifies the JWT and injects the bidang scope so handlers stay clean. Repositories then filter by allowed modules using sqlx In, while announcements handle NULL as global, and global routes require ALL access. Leaving the repository fail-open keeps public calls simple, and subtests ensure scoped admins only see their own data.
#go#mysql#security - 04:57backend
Two-Axis RBAC: Roles for Actions, Bidang for Data
Roles decide actions, bidang decides data. Notes from building a fail-closed scope resolver in Go: ModulesFor, ResolveBidang, and the NULL trap.
TL;DR: Roles handle actions while a bidang column scopes data by agency, avoiding a huge permission matrix. A helper maps bidang to modules, returns nil for unknowns to fail closed, and copies the ALL slice to prevent Go append bugs. Announcements use NULL for global posts with an IS NULL filter, and empty bidang maps to ALL for legacy JWT compatibility.
#go#mysql#security