A sweep of the services for write sequences that committed step by step,
each one leaving half a change behind when the next statement failed:
- accommodations: the linked bookings' times moved after the stay's own
transaction had committed
- airports: the boot backfill wrote a flight's two endpoints one at a
time, and a flight left with only the first was never picked up again;
each flight is its own transaction now
- auth: the admin settings form and a password reset (retire the old
link, issue the new one) each land whole
- oauth: a refresh revoked the old pair before issuing the new one, so a
failure in between logged the client out; rotating a client secret and
revoking its tokens go together too
- oidc: the identity link, the role from the claims and the avatar of a
returning user are one write
- addons, notification preferences: a settings save lands whole
- places: create with its tags, update, and the GPX/KML imports with
their track colours
- trips: the trip row, the shifted leave entries and the rebuilt day grid
- vacay: a first visit provisions the plan in one transaction with a
second look against a concurrent first visit; updatePlan fetches the
public holidays between two transactions instead of inside its writes
- memories: a photo and its trip link, and the deletes with the orphan
sweep behind them
AddonsService, AirportsService and OauthService take the UnitOfWork for
this. Network I/O stays outside every transaction. Comments that only
narrated the migration plan make room under the lint:size entries, which
drop with them.
Uninstall deleted the plugin row and then up to fourteen tables of rows
about it one by one. A failure halfway left grants, scheduled tasks or
per-user settings behind for a later plugin that reuses the id to
inherit. The rows now go in one transaction, and the data directory is
removed after the commit instead of before the rows that describe it.
A registry install records the discovered row, its provenance, the
pinned author key and the cleared update block together, so a failure
never leaves a registered plugin without its pinned key. Starting an
OAuth connect drops the stale state and stores the fresh one together,
and a disconnect drops tokens and state together; PluginOAuthService
takes the UnitOfWork for that. The scheduler's per-task claims stay
separate on purpose and say so for the transaction gate.
createJourney inserted the journey, then its owner row, then linked each
trip with its skeleton entries and set the cover, every step committed on
its own: a failure halfway left a journey nobody owned. It now runs as
one transaction and broadcasts the linked trips after the commit.
addTripToJourney links inside a transaction the same way.
The photo paths wrapped only the gallery insert and left the entry link
and the skeleton promotion outside; the photo row, the gallery row, the
link and the promotion are one write now. removeTripFromJourney,
updateEntry, updatePhoto, setPhotoProvider, the photo deletes and the
place hooks write in one transaction each.
createEntry and uploadGalleryPhotos read MAX(sort_order) inside the
transaction that inserts, so two entries or uploads at once take one
position each. reconcileTripSkeletons and the photo capture helpers are
tagged as deliberately split for the transaction gate that follows.
sql-functions.ts sat at 1007 lines, held there by the size baseline, so it
had no room for the Postgres branch every helper is meant to grow. The
Kysely-expression twins are a separate family already (a Kysely Expression
and a MikroORM RawQueryFragment do not convert into each other), so they
move to src/db/dialect/kysely-functions.ts, and the platform check plus the
fail-closed unsupported() move to platform.ts, which both files share.
No helper changes behaviour or spelling. lint:dialect now skips the whole
src/db/dialect/ directory instead of the one file, and the size baseline
drops its sql-functions.ts entry.
The repositories typed their Kysely statements against 159 table interfaces
written by hand, one or more per repository. Nothing compared them with the
schema: a migration that renamed a column or changed its type left them
stale while tsc stayed green, and several had already drifted
(reservations.accommodation_id is TEXT, not INTEGER).
scripts/generate-db-types.ts migrates a throwaway database with the real
migrations, reads every table back and writes one interface per table into
src/db/kysely/ plus the DB map in db.ts. Every hand-written interface is now
Pick<DB, ...> of that type, and npm run check:db-types fails when the
committed files no longer match the migrations.
A column an insert may leave out (rowid, DEFAULT, nullable) is
InsertOptional<T>, declared as ColumnType<T, T | void, T>. The server
compiles without strictNullChecks, where Kysely's own Generated<T> still
demands the column; that is why the old code kept separate insert-only
interfaces. The ids callers pass as number | string are cast to the column
type at the bind, so every statement binds the same value as before.
Each gate came with its npm script but ran nowhere. The server job now
runs lint:size, lint:boundaries and shared's contracts:open after the
shared build; the client job runs lint:warnings in place of lint:check
(it fails on errors the same way and also holds the warning count per
rule), then lint:format, lint:layers, lint:offline, lint:skips and
lint:i18n-keys. All of them are steps of jobs ci-ok already needs.
Both size baselines are taken on the merged branch. Four server files
grew while the other changes landed (the claimed reminder, the email
uniqueness error and the generator rule for an expression index) and
four client files by two or three lines with the plural forms; one
client file shrank and is lowered.
The client has had lint:size since #2612, the server had nothing, and
the server is where the biggest files live: maps.service.ts holds 3231
lines and grew by 200 in a month, and nine more services are past 1100.
scripts/size-lint.mjs carries the client's semantics over to src/ and
scripts/: a file may hold 1000 lines, the 23 that already hold more are
pinned at today's length in scripts/size-baseline.json, and --update
only ever lowers an entry.
A line past prettier's 120 columns counts once per 120 columns it spans,
so joining lines does not make room under the limit. The check fails
closed: a missing or malformed baseline or a missing src/ stops it
instead of passing with nothing checked.