trails/apps/journal/app/jobs/komoot-bulk-import.ts
Ullrich Schäfer 855244747c
journal: typed job seam + Komoot credentials through the manager
Two related fixes from the architecture review:

Typed job seam. Job payloads were `unknown` end-to-end: every handler
opened with `job.data as SomePayload`, every enqueue site passed a bare
string queue name and an unchecked object, and a typo meant a runtime
failure in a background worker. packages/jobs gains defineJob(), which
keeps the payload typed inside the handler and performs the
contravariance cast once inside the package. The journal declares all
14 queues and their payload shapes in app/jobs/payloads.ts; enqueue()/
enqueueOptional() and defineJournalJob() key off that map, so enqueue
sites and handlers cannot drift and queue names are compile-checked.

Komoot credential bypass. The bulk-import route enqueued the raw
credentials JSONB in the pg-boss payload, skipping withFreshCredentials
entirely: credentials sat at rest in the job table, were never
refreshed if stale, and markNeedsRelink never fired. The payload now
carries only the serviceId; the handler resolves fresh credentials
through the ConnectedServiceManager at execution time, and marks the
import batch failed (instead of leaving it pending forever) when
credential resolution itself fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:05:24 +02:00

36 lines
1.4 KiB
TypeScript

import { defineJournalJob } from "./payloads.ts";
import { logger } from "../lib/logger.server.ts";
import { withFreshCredentials } from "../lib/connected-services/manager.ts";
import {
markBatchFailed,
runKomootBulkImport,
type KomootCreds,
} from "../lib/komoot-bulk-import.server.ts";
export const komootBulkImportJob = defineJournalJob({
name: "komoot-bulk-import",
retryLimit: 1,
expireInSeconds: 1800,
async handler(jobs) {
const batch = Array.isArray(jobs) ? jobs : [jobs];
for (const job of batch) {
const { batchId, userId, serviceId } = job.data;
logger.info({ batchId, userId }, "komoot bulk import job started");
try {
// Credentials are resolved through the ConnectedServiceManager at
// execution time — the payload carries only the serviceId, so
// nothing credential-shaped sits in the job table and a relink
// between enqueue and execution is picked up here.
await withFreshCredentials(serviceId, (creds) =>
runKomootBulkImport(batchId, userId, creds as KomootCreds),
);
} catch (err) {
// runKomootBulkImport marks its own failures; this covers errors
// before it ran (service missing/not active/needs relink), where
// the batch would otherwise stay "pending" forever.
await markBatchFailed(batchId, err instanceof Error ? err.message : String(err));
throw err;
}
}
},
});