Migrate repos from raw SQL to Kysely query builder#3
Open
Migrate repos from raw SQL to Kysely query builder#3
Conversation
7527697 to
b80e0b9
Compare
Add core Kysely query builder infrastructure matching LessonsApi PR #38 pattern:
- src/db/index.ts: Multi-database Kysely factory (getDb(moduleName)) with
dialect switching via DB_DIALECT env var. MySQL uses BIT(1)→boolean typeCast.
- src/shared/infrastructure/KyselyRepo.ts: Base classes for all repos:
- KyselyRepo: church-scoped CRUD (id+churchId) with opt-in softDelete
- GlobalKyselyRepo: global CRUD (id only, no churchId)
- Both provide save, delete, load, loadAll, saveAll, insert
- convertToModel/convertAllToModel delegation (DRY: repos only override
convertToModel, base handles array mapping)
Dependencies: kysely@^0.28.14, pg@^8.13.0, @types/pg@^8.11.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ConfiguredRepo/GlobalConfiguredRepo/BaseRepo + TypedDB.query() with Kysely type-safe query builder across all repository classes. Repos converted by module: - attendance: 8 (CampusRepo, ServiceRepo, ServiceTimeRepo, SessionRepo, VisitRepo, VisitSessionRepo, GroupServiceTimeRepo, AttendanceRepo) - content: 29 (including 8 GlobalKyselyRepo for Bible/Song data) - doing: 13 (MembershipRepo kept on TypedDB — cross-module) - giving: 10 (DonationRepo, FundRepo, GatewayRepo, etc.) - membership: 26 (including 8 GlobalKyselyRepo for User/Church/OAuth) - messaging: 13 (ConnectionRepo, MessageRepo, etc.) - reporting: ReportRepo kept on TypedDB (generic SQL runner) Controller/helper fixes: - Type casts for Kysely return types in 10 controller files - UserChurchHelper: TypedDB.query → Kysely sql template Query patterns used: - Simple CRUD → Kysely query builder (selectFrom, insertInto, etc.) - Complex JOINs/subqueries → Kysely sql tagged template - IN clauses → native Kysely array support - removed column → integer (0/1) matching MySQL TINYINT(1) schema Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add missing @Injectable() to FileRepo, AuditLogRepo, DeviceRepo - Add includeRemoved param to loadOne() for restore/audit flows - Add defaultOrderBy + orderBy param to loadAll() matching old BaseRepo - Both are backward-compatible (optional params with defaults) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4d8d98b to
e933ad9
Compare
Adapt integration test suite from Drizzle branch for Kysely: - Replace getDrizzleDb → getDb, drizzle-orm sql → kysely sql - Fix db.execute(sql`...`) → sql`...`.execute(db) (Kysely API) - Remove assertions on DB-generated fields (NOW(), auto-computed) that aren't on the model returned by save() - Fix repo bugs found by tests: AccessLogRepo/AuditLogRepo create path, GroupRepo labelArray cleanup, QuestionRepo sort on delete All 299 tests pass across 6 modules: attendance: 36 | content: 62 | doing: 59 giving: 58 | membership: 51 | messaging: 33 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Restore convertToModel on ConversationRepo, MessageRepo, ConnectionRepo to match original rowToModel field whitelisting (prevents leaking extra DB columns in API responses) - Remove unused `sql` import from UserRepo, SettingRepo - Use this.createId() consistently in base class save/insert instead of direct UniqueIdHelper.shortId() calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port MySQL DDL to PostgreSQL for: membership (30), attendance (8), content (30), giving (11), messaging (16), doing (12). Key differences from MySQL DDL: - BOOLEAN columns instead of BIT(1)/TINYINT for flags - TIMESTAMP instead of DATETIME - SERIAL for auto-increment columns - PostgreSQL-native stored functions (cleanup, deleteForChurch, updateConversationStats) - Double-quoted identifiers throughout - DROP TABLE IF EXISTS CASCADE for idempotent re-runs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make all 99 Kysely repos work on both MySQL and PostgreSQL:
Boolean columns (PG DDL uses BOOLEAN, not BIT/SMALLINT):
- KyselyRepo base: soft-delete uses true/false not 1/0
- All .where("removed", "=", 0) → false, "=", 1 → true
- Raw SQL: removed=0 → removed=false across all modules
- Insert defaults: removed: 0 → removed: false
Dialect-specific SQL with getDialect() branching:
- AttendanceRepo.loadTrend: STR_TO_DATE → DATE_TRUNC
- DonationRepo.loadSummary: STR_TO_DATE → DATE_TRUNC
- SessionRepo.loadByGroupIdWithNames: DATE_FORMAT → TO_CHAR
- ServiceRepo.searchByCampus: rewrite to Kysely query builder
- ServiceTimeRepo.loadByChurchCampusService: rewrite to builder
- ConversationRepo: dialect-specific cleanup/save
- RegistrationRepo: INSERT...FROM dual → bare SELECT (PG)
- StreamingServiceRepo: recurring boolean filter
- TaskRepo: IFNULL → COALESCE
Cross-dialect fixes:
- SongDetailRepo: title + ' ' + artist → concat()
- RegistrationRepo.countActiveForEvent: Number() cast for PG bigint COUNT
- Remove debug console.log from Environment.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- setup.ts: add DB_DIALECT=postgres support with PG connection defaults - Fix backtick identifiers → double-quoted (groups, end) - Fix INSERT IGNORE → dialect-conditional ON CONFLICT DO NOTHING - Fix 10-char test IDs → 11-char for PG char(11) padding - All 299 tests pass on both MySQL and PostgreSQL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 tasks
The seed data file must load after roles.sql and rolePermissions.sql are created. Alphabetical ordering caused it to run too early on PG. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RolePermissionRepo: add all SELECT columns to GROUP BY (PG requires non-aggregated columns to appear in GROUP BY, MySQL doesn't enforce). PersonRepo: quote "optedOut" in raw SQL filter clause, add "churchId" to GROUP BY in loadByName. Add PostgreSQL demo data for all 6 modules (membership, attendance, content, giving, messaging, doing) with: - Double-quoted identifiers - BOOLEAN literals (true/false not 0/1) - PG date functions (EXTRACT, CURRENT_DATE, INTERVAL arithmetic) - ON CONFLICT DO NOTHING for idempotency - TRUNCATE CASCADE for re-runnability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the raw SQL pattern (
ConfiguredRepo/GlobalConfiguredRepo/TypedDB.query()) with Kysely type-safe query builder across all repository classes. This aligns with the ChurchApps maintainer's decision to adopt Kysely (Api PR #22 comment) and matches the pattern from the merged LessonsApi PR #38.Commit structure
feat: add Kysely ORM infrastructurefeat: convert 99 repos to Kyselyfix: review fixestest: add 299 integration testsfix: restore convertToModel, cleanupfeat: add PostgreSQL DDL (107 tables)fix: dual MySQL/PostgreSQL compatibilitytest: fix tests for dual dialectInfrastructure (
src/db/index.ts)getDb(moduleName)— singleton per moduleDB_DIALECTenv var:MysqlDialect(default) orPostgresDialectdestroyAllDbs()with error isolation for clean shutdownBase classes (
src/shared/infrastructure/KyselyRepo.ts)KyselyRepo— church-scoped (id + churchId): save, delete, load, loadAll, saveAll, insertsoftDelete: setsremoved=trueon delete, auto-filters on loadloadOne(churchId, id, includeRemoved?)for restore/audit flowsloadAll(churchId, orderBy?)with optionaldefaultOrderByGlobalKyselyRepo— global (id only): save, delete, load, loadAll, saveAllthis.createId()consistently (notUniqueIdHelper.shortId()directly)PostgreSQL support
tools/dbScripts/pg/— native BOOLEAN, TIMESTAMP, SERIALgetDialect()for incompatible SQL:STR_TO_DATE(concat(year(),week()))→DATE_TRUNC('week', ...)DATE_FORMAT→TO_CHARIFNULL→COALESCEINSERT IGNORE→ON CONFLICT DO NOTHINGINSERT ... FROM dual→ bareSELECT(PG)removed=0→removed=false,=1→=trueRepos converted (99/101)
| Module | Count | Notable |
|--------|-------|---------|-
| attendance | 8 | AttendanceRepo uses
sqltemplate for analytics || content | 29 | 8 GlobalKyselyRepo for Bible/Song data |
| doing | 13 | MembershipRepo stays TypedDB (cross-module) |
| giving | 10 | DonationRepo has complex date/KPI queries |
| membership | 26 | 8 GlobalKyselyRepo for User/Church/OAuth |
| messaging | 13 | ConversationRepo/MessageRepo preserve field-stripping |
| reporting | — | ReportRepo stays TypedDB (generic SQL runner) |
Not included (follow-up)
initdb.tsstill uses raw SQL files)Test plan
npx tsc --noEmit— zero errors🤖 Generated with Claude Code