Repository navigation
fix(cli): honor per-entity table override in TS generator (#98) - #106
Conversation
The `.apsorc` per-entity `table` override was dropped entirely: the
TypeScript generator always emitted `@Entity('<snake_case(name)>')` and
never read `entity.table`. It "worked" for some entities only when their
override happened to equal snake_case(name); for an entity named `Order`
with `"table": "order_record"` it generated the reserved word `order`.
- Add `table?: string` to the `Entity` type (the field existed in
.apsorc at runtime but was untyped and never surfaced).
- Generator: emit `tableName = entity.table || snakeCase(name)` and use
it in the `@Entity(...)` decorator.
- Migrate sandbox: honor the same override so local migration tests use
the same table name as the generated entity.
- Tests: override is honored; default snake_case still applies with no
override; compound names still snake_case correctly.
Note: Python/Go generators derive table names the same way and have the
same latent bug; left out of this PR to keep it focused on the reported
TypeScript/TypeORM target. Worth a parity follow-up.
Closes #98
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cultron
left a comment
There was a problem hiding this comment.
Verdict: Approve (peer review, apso-overnight-1)
Correctness: the diff matches the write-up exactly — table?: string added to the Entity type, generator computes tableName: entity.table || snakeCase(name), template switches @Entity('<%= it.snakeCasedName %>') → @Entity('<%= it.tableName %>'), and migrate/sandbox.ts honors the same override so local migration tests agree with the generated entity.
Coverage: 3 tests — override honored (order_record, never order), no-override fallback (order), compound-name regression (order_item). The last two are the important ones because they pin that the default path is unchanged.
Release-safety: for entities without a table override, entity.table is undefined → snakeCase(name), byte-identical to before, so existing schemas are unaffected; only the (previously-broken) override case changes. Safe to publish.
Missed cases: Python and Go derive table names the same way and share the latent bug — correctly scoped out as a follow-up in the PR. Fine to land TS first.
…#107) Parity with #98/#106 (TypeScript). The Python and Go generators derived the table name as snakeCase(entity.name) and never read the per-entity `table` override, so an entity named `Order` with "table": "order_record" generated the SQL reserved word `order`. - Both generators now emit `tableName = entity.table || snakeCase(name)`. - Python template uses it for `__tablename__`, the self-referencing ManyToMany ForeignKey, join-table names, and index names, so every reference to this entity's own table follows the override consistently. - Go template uses it in `TableName()`. Without an override tableName === snakeCase(name), so output is unchanged for the common case. Added override + fallback tests to both suites. Full suite: 318 passed, 0 failed. fixes #107
… Python/Go (supersedes #119) (#150) * fix(cli): honor per-entity table override in Python and Go generators (#107) Parity with #98/#106 (TypeScript). The Python and Go generators derived the table name as snakeCase(entity.name) and never read the per-entity `table` override, so an entity named `Order` with "table": "order_record" generated the SQL reserved word `order`. - Both generators now emit `tableName = entity.table || snakeCase(name)`. - Python template uses it for `__tablename__`, the self-referencing ManyToMany ForeignKey, join-table names, and index names, so every reference to this entity's own table follows the override consistently. - Go template uses it in `TableName()`. Without an override tableName === snakeCase(name), so output is unchanged for the common case. Added override + fallback tests to both suites. Full suite: 318 passed, 0 failed. fixes #107 * fix(python): point ManyToOne and join-table FKs at the parent's real table PR #119 moves __tablename__ to the per-entity table override, but child models still wrote ForeignKey("<parent name lowercased>.id"). With an override on the parent, or any multi-word parent (UserProfile -> user_profile, FK said userprofile), the FK named a table that does not exist. Pass every entity's table name into the model template and use it. * feat(test smoke): create, read and delete one row per entity apso test smoke only called GET /health and each list route. It now builds a payload per entity from the .apsorc (required fields only: not nullable, no default), creates entities parents first so required ManyToOne foreign keys (<camelCase(to_name or parent)>Id) can be set from the parent's id, reads each row back by id, and deletes children before parents. Entities it cannot fill (unsupported required types, cycles of required relationships, children of skipped entities) get a skip step with the reason instead of failing. The planner lives in src/lib/verify/crud-plan.ts with unit tests for the payload values and the ordering. * feat(test smoke, migrate): Python and Go support where it is honest apso test smoke for Python and Go projects starts the generated server (uvicorn from .venv, or a go build ./cmd binary so stopping it cannot orphan the server) and runs the same HTTP checks against DATABASE_URL from the environment or .env. It never creates a database or tables; with no DATABASE_URL, or nothing listening at its host and port, the check is skipped with a message and reports "Smoke test skipped.". Routes follow each generator: /api/<plural> (Python) and /api/<kebab-plural> (Go). apso migrate generate and verify fail fast for Python and Go and name the native tool (Alembic, goose). The baseline, run and drift flow needs a scratch database; TypeScript has PGlite, the others do not. scaffold-smoke.sh: the go leg runs apso test smoke when SMOKE_DATABASE_URL points at an empty Postgres (opt in, off in CI).
Summary
Fixes #98: the
.apsorcper-entitytableoverride was silently ignored, so an entity namedOrderwith"table": "order_record"still generated the SQL reserved wordorder.Root cause
The override was dropped at two layers:
tablewasn't declared on theEntitytype, so it was never surfaced from the parsed.apsorc(it existed on the raw object at runtime but was untyped).snakeCase(name)and never readentity.table. The entity template emitted@Entity('<%= it.snakeCasedName %>').The reporter's "works for other compound-name entities" observation was a red herring: it only appeared honored when the override happened to equal
snakeCase(name)(e.g.OrderItem→order_item). ForOrder→order_recordthe mismatch made the bug visible.Fix
table?: stringto theEntitytype.tableName = entity.table || snakeCase(name)and the template uses it in@Entity(...).migrate/sandbox.tshonors the same override so local migration tests match the generated entity's table name.Test
Added
table name override (cli#98)to the TS generator suite:@Entity('order_record'), never@Entity('order')@Entity('order')preserved@Entity('order_item')(regression guard)npm run test: 208 passed / 0 failed.Scope note
The Python and Go generators derive table names the same way (
snakeCase(name)) and share this latent bug. I kept this PR to the reported TypeScript/TypeORM target to stay focused and low-risk; the same one-line pattern would give Python/Go parity as a follow-up.Note
Merging to
mainpublishes@apso/cli(merge = release) — leaving the merge to Matt.Closes #98
🤖 Generated with Claude Code