Skip to content

fix: generated projects are broken — repair the generator, harden secrets, prove it in CI - #1

Open
tashikomaaa wants to merge 7 commits into
mainfrom
fix/generated-code-correctness
Open

tashikomaaa wants to merge 7 commits into
mainfrom
fix/generated-code-correctness

Conversation

@tashikomaaa

Copy link
Copy Markdown
Owner

Why

aapi generate model <Name> --relations ... produced a Mongoose model that does not parse:

author: { type: mongoose.Schema.Types.ObjectId, ref: &#39;User&#39; },
//                                                   ^ SyntaxError

Pulling that thread turned up a chain of defects across the generator, the templates and the security mode. Four out of six generate flags were broken, generated tests could never run, and the default Yoga server died on its first /health request.

None of it was caught because CI only listed the generated files and printed package.json — it never parsed, installed, ran or started anything it produced.

What was broken

Reported first, fixed here

Defect Effect
Relation fragments interpolated with EJS <%= %> Models and resolvers are invalid JavaScript whenever --relations is used
--with-subscriptions / --with-cache import utils/pubsub.js and utils/cache.js Those files were never written by any command — the project cannot start
--no-tests read options.withTests Commander maps the flag to options.tests, so it never had any effect
Generated tests import ../../models/… from src/__tests__/ Wrong depth; plus jest.config.js/jest.setup.js were never copied and no test script or Jest dependency was declared
Schema and resolver loaders glob from process.cwd() Empty schema unless the server is started from the project root (breaks Docker, PM2, monorepos)
--secure falls back to a hardcoded signing key An API deployed without a .env signs tokens with a key published in this repo: anyone can forge an admin token

Found only by actually booting the generated servers

Defect Effect
/health registered as a second request listener alongside Yoga Both listeners run per request; the second throws ERR_HTTP_HEADERS_SENT and kills the process on the first health check — i.e. the first load-balancer probe
pubsub.js imports PubSub from graphql-yoga v5 exports createPubSub; subscriptions could never work
cacheResolver reads context.skipCache unconditionally Crashes whenever a resolver is called without a context
Cache cleanup setInterval never unref()ed Keeps the process and Jest workers alive forever

Smaller

--skip-install skipped nothing (install never ran at all) · --secure --apollo silently dropped security · .gitignore/.editorconfig templates absent from the npm tarball (npm always strips .gitignore) · aapi list reported the schema's state on the "Model" line · aapi --version hardcoded · Windows-broken new URL().pathname · apollo-server-express@3 EOL since Oct 2023 · stray Product artifacts committed into the CLI's own src/ and published to npm · no LICENSE despite the badge.

What changed

  • Security: the --secure server now refuses to start in production when a JWT secret is missing, still the placeholder, under 32 characters, or identical between access and refresh tokens. Outside production it warns instead of blocking.
  • Apollo path migrated from the EOL apollo-server-express v3 to @apollo/server v4 + @as-integrations/express4.
  • aapi create now actually installs dependencies; --skip-install genuinely opts out.
  • --with-subscriptions fails with an actionable message on non-Yoga projects rather than emitting dead imports (Apollo Server 4 needs graphql-ws wiring AAPI does not scaffold).
  • Generated projects ship a working Jest setup backed by an in-memory MongoDB.

Verification

Two layers, both in CI:

  • __tests__/commands/generate.integration.test.js — scaffolds projects and asserts every generated file parses under node --check, with targeted guards per regression. Mutation-tested: reverting the <%- fix makes it fail.
  • npm run verify:generated / verify:generated:apollo — scaffold → generate → type-check → install → run the project's own suite → boot the server from / → hit /health twice → CRUD round-trip over GraphQL.

Starting outside the project root is what catches cwd-dependent loading; hitting /health twice is what catches the duplicate listener.

Results on both server flavours:

▶ Type-checking every generated file
  16 files parse
▶ Generated project test suite
Test Suites: 3 passed, 3 total
Tests:       43 passed, 43 total
▶ Booting the server from "/" against an in-memory MongoDB
  GET /health -> {"status":"ok",...,"mongodb":"connected"}
▶ CRUD round-trip over the generated schema
  posts -> {"items":[{"name":"verify"}],"pageInfo":{"total":1}}
✅ generated project builds, tests, boots and serves requests

Repo itself: lint clean, 59/59 tests.

Reviewing

Seven thematic commits, each self-contained — git log -p main..HEAD reads in order. Two deliberate exceptions to one-theme-per-commit, called out in the commit bodies: init.js carries the Apollo 4 dependency list in the CLI commit, and the Apollo package.json.ejs carries its test script in the security commit. That beat hunk-level splitting, which does not play well with lint-staged on partially-staged files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW

tashikomaaa and others added 7 commits September 20, 2026 11:04
Relation fragments (Mongoose field definitions, GraphQL fields, the
populate chain) were interpolated with EJS `<%= %>`, which HTML-escapes
its output. Every quote in the generated code became `&#39;`:

    author: { type: mongoose.Schema.Types.ObjectId, ref: &#39;User&#39; },

so `aapi generate model <Name> --relations ...` produced a model and a
resolver that fail to parse, and the scaffolded project could not start.

These fragments are code, not user-facing text, so they are now injected
with `<%- %>`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
Several `aapi generate model` flags produced code that could not run:

- `--with-subscriptions` / `--with-cache` generated a resolver importing
  `../../utils/pubsub.js` and `../../utils/cache.js`, but no command ever
  wrote those files. They are now rendered next to the resolver that
  needs them, and never overwrite an existing file.
- `--no-tests` had no effect: commander maps it to `options.tests`, while
  the code read `options.withTests`, which is always undefined from the
  CLI.
- Template paths were resolved with `new URL(import.meta.url).pathname`,
  which yields a broken path on Windows. Now uses `fileURLToPath`, as
  `create` already did.

The two runtime modules were themselves unrunnable, which is why nobody
noticed they were missing:

- both called `require()` from an ES module (ReferenceError as soon as
  REDIS_URL was set); they now build one via `createRequire`.
- `pubsub.js` imported `PubSub` from `graphql-yoga`, which v5 does not
  export — the API is `createPubSub`.
- `cacheResolver` read `context.skipCache` unconditionally and crashed
  whenever a resolver was called without a context, as tests do.
- the memory-cache cleanup interval was never `unref()`ed, so it kept the
  process (and Jest workers) alive forever.

Subscriptions depend on the Yoga PubSub and Apollo Server needs transport
wiring AAPI does not scaffold, so `--with-subscriptions` now fails with an
actionable message on a non-Yoga project instead of emitting dead imports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
Tests could never run. The generated test file lands in `src/__tests__/`
but imported `../../models/<Name>.js`, one level too high; `jest.config.js`
and `jest.setup.js` existed as templates but were never copied into the
project; the default package template declared no `test` script and no
Jest dependency, while the secure one used plain `jest`, which cannot
load ES modules without `--experimental-vm-modules`.

Generated projects now ship a working Jest setup backed by an in-memory
MongoDB, so `npm test` passes on a freshly scaffolded project. The setup
falls back to MONGODB_URI_TEST when `mongodb-memory-server` is absent.
When `--with-cache` is on, the generated tests clear the cache between
cases: cached results otherwise outlive the documents they were built
from and the suite fails on stale data.

Two server-side defects:

- `typeDefs/index.js` and `resolvers/index.js` globbed from
  `process.cwd()`, so the schema was empty unless the server was started
  from the project root — breaking Docker, PM2 and monorepo layouts.
  Both now resolve from their own module location.
- `/health` was registered as a *second* `request` listener on a server
  already handled by Yoga. Both listeners ran for every request and the
  second one threw ERR_HTTP_HEADERS_SENT, killing the process on the
  first health check — exactly what a load balancer probe does. Health
  and Yoga now share one listener, and /health answers 503 when MongoDB
  is disconnected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
…and version

- `aapi create` never ran `npm install`. The `--skip-install` flag only
  changed which hint was printed, so it skipped nothing. Dependencies are
  now installed by default and the flag genuinely opts out; a failed
  install reports the tail of npm's output instead of failing silently.
- `--secure --apollo` silently produced a non-secure Apollo project:
  security templates are Yoga-only. The combination now warns and says
  how to get the secure template, and the security files are no longer
  written next to a server that never loads them.
- `.gitignore` and `.editorconfig` templates never reached scaffolded
  projects installed from npm: npm always strips files named
  `.gitignore`, and `.npmignore` excludes `.editorconfig`. Both templates
  are stored undotted and renamed on copy. A missing template file is now
  an error rather than a silent skip.
- `aapi list` printed the *schema's* existence on the "Model" line, so a
  model file deleted by hand still showed as present.
- `aapi --version` was hardcoded to 0.1.0 and would drift from
  package.json; it now reads the real version.

`init` also learns the jest wiring (config files, `test` script and dev
dependencies) so `aapi init` and `aapi create` produce the same setup,
and its Apollo dependency list is updated alongside the Apollo Server 4
migration in the following commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
`--secure` projects fell back to a hardcoded signing key when the
environment was not configured:

    process.env.ACCESS_TOKEN_SECRET || 'your-access-secret-change-in-production'

That placeholder ships in this repository, so an API deployed without a
.env signed its tokens with a publicly known key: anyone could forge an
admin token. The failure was silent, which is the worst property a
secret-handling default can have.

The generated server now refuses to start in production when a secret is
missing, still the scaffolded placeholder, shorter than 32 characters, or
identical between access and refresh tokens (which would let a refresh
token be replayed as an access token). Outside production it warns on
every start instead of blocking local work.

Also migrates the `--apollo` path off `apollo-server-express` v3, which
reached end of life in October 2023 and receives no security patches, to
`@apollo/server` v4 with `@as-integrations/express4`. The Apollo package
template picks up the `test` script and Jest dependencies introduced
earlier in this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
AAPI is a code generator, but nothing checked its output. Unit tests over
template inputs cannot see an EJS escaping bug, a missing import target or
a server that dies on its own health check — all three shipped.

Two layers:

- `__tests__/commands/generate.integration.test.js` scaffolds projects in
  a temp directory and asserts every generated file parses under
  `node --check`, plus targeted guards for the regressions just fixed
  (escaped quotes, emitted runtime modules, `--no-tests`, test import
  depth). Verified by mutation: reverting the `<%-` fix makes it fail.
- `scripts/verify-generated-project.mjs` goes further and is what CI now
  runs for both server flavours: scaffold, generate, type-check, install,
  run the project's own suite, boot the server **from `/`** against an
  in-memory MongoDB, hit /health twice and do a CRUD round-trip over
  GraphQL. Starting outside the project root is what catches
  cwd-dependent loading; hitting /health twice is what catches a
  duplicate request listener.

CI previously only listed the generated files and printed package.json,
which is why every one of these bugs passed it.

Also fills the empty `author` field in package.json and keeps `scripts/`
out of the published package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
- Adds the MIT LICENSE file the README has been advertising with a badge
  since the first commit.
- Removes `src/models/Product.js`, `ProductResolver.js` and
  `Product.graphql`: generated output accidentally committed into the
  CLI's own source tree and published to npm with every release.
- Removes the empty `src/utils/fs.js` placeholder.
- README no longer tells users to run `npm install` after `aapi create`,
  which now installs for them, and spells out that the secure template
  requires real JWT secrets.
- CHANGELOG records the fixes in this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao3xH9yfS95UgmBm1VquAW
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