fix(cli): lowercase the postgres database name derived from the app name - #1330
Conversation
`webjs create MyApp --db postgres` wrote the app name into the emitted DATABASE_URL with a case-preserving replace, so the URL named `MyApp`. PostgreSQL folds an unquoted identifier, so `CREATE DATABASE MyApp;` creates `myapp` and the emitted URL then fails to connect. `createdb MyApp` goes the other way and creates `MyApp` literally, because it quotes through fmtId. The real defect is that the emitted name was not quoting-invariant, so which database you got depended on which creation route you took. Derive it once in a new pure `toDatabaseName()` in app-name.js: fold with toLowerCase (never toLocaleLowerCase, which is locale-dependent), map every remaining non-[a-z0-9_] character to `_` one for one, prefix a single `_` when the result starts with a digit, then cap at 63 bytes, PostgreSQL's NAMEDATALEN - 1. The order is load-bearing: the fold before the slice makes the byte cap exact, and the prefix before the slice makes the cap govern the final string. The app name itself is untouched everywhere else. Only that one URL segment changes, and the post-scaffold guidance now names the database so the user knows what to create.
A name folding to a keyword (order, user, table) is still not quoting-invariant: CREATE DATABASE order; is a syntax error while createdb order succeeds, because fmtId quotes a keyword. The JSDoc stated the property without that qualification. Not detected on purpose, and the comment now says why.
vivek7405
left a comment
There was a problem hiding this comment.
Read the whole diff. The derivation is right and the fold-before-slice ordering is the part I wanted to check hardest, since that is what makes the 63 cap a byte cap rather than a code-unit one, and it holds. Hoisting dbName so the emitted URL and the printed guidance cannot drift is the right call, and I like that the no-regression case (my-pg still yields my_pg) is pinned as its own assertion rather than folded into the mixed-case one, because that asymmetry is what makes the counterfactual meaningful.
One real problem, on the JSDoc rather than the code: the quoting-invariance property is stated without qualification and there is a case it does not cover. Inline.
The thing I would keep an eye on is scope creep into reserved-word or collision handling. Both are genuinely out of scope here and the issue argues that well, so the right move is to say so in the comment rather than to start detecting anything.
Closes #1255
webjs create MyApp --db postgreswrote the app name into the generated.env.exampleDATABASE_URLwith a case-preserving replace (name.replace(/[^a-z0-9_]/gi, '_'), where theiflag is what let capitals through). PostgreSQL folds an unquoted identifier, soCREATE DATABASE MyApp;createsmyappand the emitted URL then fails withdatabase "MyApp" does not exist.createdb MyAppgoes the other way and createsMyAppliterally, because it quotes throughfmtId. So the real defect is broader than a wrong URL: the emitted name was not quoting-invariant, and which database you ended up with depended on which creation route you took.What changed
packages/cli/lib/app-name.js: new pure exportstoDatabaseName(name)andDB_NAME_MAX_LENGTH(63). The rule is fold withtoLowerCase(), map every remaining non-[a-z0-9_]character to_one for one, prefix a single_when the result starts with a digit, then slice to 63. The order is load-bearing: the fold before the slice makes the byte cap exact (every surviving character is one ASCII byte), and the prefix before the slice makes the cap govern the final string. The module still imports nothing.packages/cli/lib/create.js: hoistsconst dbName = toDatabaseName(name)and uses it in theDATABASE_URLline, and the post-scaffold Postgres guidance now names that database so the user knows what to create. The idempotent replace-or-append block below it is unchanged.The app name itself is untouched everywhere else: the directory, the
package.jsonname, the{{APP_NAME}}substitution,displayName,metadata.title. Only that one URL segment is normalized. The sqlite arm is a fixedfile:./db/dev.dbwith no name interpolation, so it has no exposure.Deliberately excluded, all argued in the issue: no reserved-word detection (the list is version-dependent and roughly 470 entries, and
createdb useralready works becausefmtIdquotes it; the JSDoc now states that carve-out explicitly, since a name folding to a keyword is the one case the quoting-invariance property does not cover), no collision detection (My-Appandmy_appboth fold tomy_app, but this is a placeholder in.env.example, not a provisioned resource, and a uniquifying suffix would make the name untraceable), no camel split (Rails splits, but the goal is the name PostgreSQL itself folds to, and a separator PostgreSQL would not insert is a mismatch in the other direction), and no empty-result fallback (scaffoldAppassertscheckAppNamebefore any file is written, so a validated name's first character always survives the fold).Test plan
packages/cli/test/app-name/app-name.test.js): a derivation table (MyApptomyapp,TaskFlowtotaskflowwith no split,My.App-2tomy_app_2,my-pgtomy_pgunchanged,2appto_2app), the 63 cap including the fold-before-slice and prefix-before-slice cases, idempotence, and a shape property asserting every name the existing accept-list declares valid derives a non-empty/^[a-z_][a-z0-9_]*$/string. The accept-list is hoisted toVALID_NAMESso the property runs over exactly that corpus.test/scaffolds/scaffold-integration.test.js): scaffoldsMyPgAppand asserts the exact anchored lineDATABASE_URL=postgres://user:password@localhost:5432/mypgappplus that the printed guidance namesmypgapp(via a newcaptureConsole()sibling tomuteConsole(), so no existing caller changes), and scaffoldsmy-pgasserting the byte-identical-to-before.../my_pg.giregex reds theMyPgAppassertion and leaves themy-pgone green. That asymmetry is what proves the test targets the bug rather than the mechanism.webjs create MyPgApp --db postgres --no-installemitsDATABASE_URL=postgres://user:password@localhost:5432/mypgapp, the banner prints "The example URL names the databasemypgapp", andwebjs checkinside the generated app passes. The--runtime bunvariant emits a byte-identical.env.example.npm test: 4064/4070 pass. The 5 failures are the elision differential tests andtest/bun/listener.test.mjs, none of which this diff can reach. They are a worktree artifact:packages/core/distthere is a symlink to the primary checkout, and the same tests pass in the primary. CI builds from the branch, so it is unaffected./,/docs/database,/uiand/ui/buttonwith no broken modulepreload hints.examples/blogis N/A: this diff is thewebjs createscaffold writer and changes nothing either in-repo app serves..env.example, which is never served, never imported, and never reaches a page, layout, or component. No route, request path, or navigation behaviour changes.node:crypto, or TS-stripper surface. The parity hook's keyword filter does not matchcreate.jsorapp-name.js, and the--runtime bungeneration check above covers the one bun-adjacent surface.Docs
packages/cli/AGENTS.mdtoDatabaseNameadded to theapp-name.jsmodule-map entry with the quoting-invariance rationale;Tests:line extended.website/app/docs/database/page.tsAGENTS.mdDATABASE_URLappended to the scaffolding paragraph that already documents the uppercase carve-out.Checked and deliberately not changed:
.agents/skills/webjs/references/built-ins.mdandwebsite/app/docs/configuration/page.tsmentionDATABASE_URLonly as an env-var example,packages/cli/templates/.env.examplecarries the sqlite default thatcreate.jswrites over,packages/cli/lib/api-gallery.jsmentions it only as anenv.tsschema key. README and CONVENTIONS.md: N/A, not a headline capability and no convention changed.MCP, editor plugins, marketing copy, scaffold templates, version bumps: N/A. No introspection surface, grammar, snippet, landing-page claim, or template content changes.