Skip to content

feat(laddr-import): one-shot migration script from laddr mysqldump#24

Merged
themightychris merged 5 commits into
mainfrom
feat/laddr-import
May 16, 2026
Merged

feat(laddr-import): one-shot migration script from laddr mysqldump#24
themightychris merged 5 commits into
mainfrom
feat/laddr-import

Conversation

@themightychris
Copy link
Copy Markdown
Member

Summary

  • One-shot CLI (npm run -w apps/api script:import-laddr) reads a laddr mysqldump and writes records to both the public gitsheets repo and the private filesystem store
  • Idempotent on legacyId (and on composite paths for memberships + tag-assignments which have no legacyId field) — re-running against the same dump produces zero new commits
  • Seeds Person.slackSamlNameId from laddr Username, splits tag handles like topic.transit, normalizes stage values, slugifies invalid slugs with dedupe suffixes
  • Emits one commit per entity type (7 total) authored as Code for Philly API <api@users.noreply.codeforphilly.org> with Action: import.laddr, Source-Dump, Run-At trailers per specs/behaviors/legacy-id-mapping.md
  • Synthetic fixture at apps/api/scripts/fixtures/laddr-fixture.sql + a Vitest suite cover the validation criteria in plans/laddr-import.md

Implements plan: laddr-import.

Test plan

  • Run against the synthetic fixture → expected records in public repo + private store
  • Re-run against the same dump → no-op (zero new files, zero new commits)
  • --limit=1 truncates per-table to 1 imported
  • --dry-run produces JSON report with no writes
  • Person.slackSamlNameId populated correctly for every Person; matches their slug
  • Stage values translated (TitleCase → lowercase)
  • Person.email + LegacyPasswordCredential.passwordHash land in the private store, not the public repo (regex audit: zero hits)
  • Tag handles like topic.transit split correctly into namespace='topic', slug='transit'
  • tag_items.ContextClasstaggableType mapping correct
  • Drop-tables (member_checkins) skipped without error
  • npm run lint, type-check, test, build all green

themightychris and others added 4 commits May 16, 2026 17:54
Custom line-streaming parser for the narrow CREATE TABLE / INSERT INTO
grammar laddr dumps use; avoids loading the whole SQL file into memory.
Includes a 10-row synthetic fixture covering all migrated entities
(people, projects, memberships, updates, buzz, tags, tag-assignments)
plus one dropped table (member_checkins) to verify it's skipped without
error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alization

Per-table translator functions that map a raw laddr row into a v1
record (Zod-shape, not yet validated). Mints UUIDv7s, threads them
through cross-table id maps for FK resolution, and emits warnings for
soft-fixed conditions (invalid slugs, unknown stages, dropped context
classes).

Normalization rules pulled from data-model.md:
  - slugs slugified + dedupe-with-suffix when source doesn't match the
    new regex (default per plan; document in Notes)
  - stage TitleCase → lowercase, unknowns fall back to "commenting"
  - tag handles like "topic.transit" split into namespace + slug
  - ContextClass "...Project" / "...Person" → taggableType
  - Email + password hash routed to PrivateProfile +
    LegacyPasswordCredential separately (never in the public side)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The orchestrator (importer.ts) drives FK-respecting passes — tags →
people → projects → memberships → updates → buzz → tag-assignments —
emitting one gitsheets commit per entity type (7 total). Each commit
uses the pseudonymous "Code for Philly API" identity per
specs/behaviors/storage.md, with Action/Source-Dump/Run-At trailers per
specs/behaviors/legacy-id-mapping.md.

Idempotence: before any pass, walks the data-repo's git tree to
collect existing legacyId → (id, slug) maps for the five sheets with
legacyId, plus composite-path sets for memberships + tag-assignments
(which have no legacyId field). Re-running against the same dump on
an already-imported repo produces zero new files and zero commits.

Private store: PrivateProfile records flow through PrivateStoreTx;
LegacyPasswordCredential records (one-shot migration data, never
written by the runtime API) are seeded via direct flush onto
BasePrivateStore's internal legacyPasswords map — keeping the runtime
interface narrow.

CLI (`npm run -w apps/api script:import-laddr`) accepts --sql,
--data-repo, --private-store, --dry-run, --verbose, --limit.

Tests cover: dry-run report shape, full-import end-to-end with PII
audit (no email patterns or bcrypt hashes anywhere in the public
tree), tag namespace splitting, stage normalization, composite-path
shapes, per-project ProjectUpdate.number sequencing, second-run
no-op, and --limit truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@themightychris themightychris merged commit 65e964e into main May 16, 2026
1 check passed
@themightychris themightychris deleted the feat/laddr-import branch May 16, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant