Skip to content

feat(projects): offer a connection URL in create:sql / create:nosql prompts - #807

Open
Gawtier wants to merge 4 commits into
mainfrom
feat/create-prompt-connection-uri
Open

feat(projects): offer a connection URL in create:sql / create:nosql prompts#807
Gawtier wants to merge 4 commits into
mainfrom
feat/create-prompt-connection-uri

Conversation

@Gawtier

@Gawtier Gawtier commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What

Interactive projects:create:sql / projects:create:nosql now open with an optional connection URL promptDatabase connection URL (leave blank to enter the details manually): (MongoDB wording on nosql). Paste a URL and the field prompts (dialect/name/host/port/user/password, Mongo SRV) are skipped; leave it blank and today's field-by-field flow runs unchanged.

This brings the CLI to parity with the web onboarding, which offers the same Form/URL choice. Until now the connection URL was reachable only via the -c flag (prompter: null), so a human running the interactive create could never paste a connection string.

How it works

Scoped to the two commands — the shared options are untouched, so init / projects:create / demo prompts are unchanged:

  • Each command overrides databaseConnectionURL with a leading prompter and a per-engine validator (validateSqlConnectionUrl / validateMongoConnectionUrl in src/services/projects/create/options.ts). No new flag, no extra "choose a method" question.
  • The field prompts get when: skipWhenConnectionUrl, so they are skipped once a URL is answered.
  • The URL option carries an exclusive list mirroring the field options': passing any DB field flag (-d/-n/-h/-p/-u/--databasePassword, --mongoDBSRV) drops the URL question, so scripted/non-TTY usage never faces a new question to hang on.
  • The prompt is masked (prompter.secret, new): the URL embeds the database password, and optionToInquirer only masks options whose name matches /(password|secret)/. Without it the credentials were echoed on screen and left in the terminal scrollback — a regression against the field-by-field flow, whose databasePassword prompt is masked. A * mask keeps a failed paste distinguishable from the blank answer.
  • The answer is trimmed at the source (filter, new — inquirer runs it before when/validate). A pasted URL routinely drags whitespace along, and the untrimmed value reaches getDialect() during prompting, so a leading space used to make the database schema question disappear. getCommandOptions normalizes the -c flag path the same way, and a blank answer becomes undefined so the DB config falls back to the field values.

Behaviour change for scripted usage (-c)

getCommandLineOptions runs an option's validate on values coming from the command line too, so the new validators also gate the -c flag. projects:create:sql -c mariadb://… now fails at parse time where it used to be accepted.

This is deliberate. runAuthenticated creates the project on Forest before testing the database connection, so a URL the toolbelt cannot interpret either leaves an orphan project behind, or — worse — succeeds all the way and hands the user a generated project whose package.json ships no driver, broken at the first npm start. Failing in the parser, with a message that says what to do, is the point.

What is rejected, and why:

  • mariadb://getDialect() maps it to the mysql dialect, so the generated project ships mysql2, while @forestadmin/datasource-sql derives mariadb from the URL scheme and needs the mariadb driver. Use mysql://. (The dumper does have mariadb support, but getDialect() never returns that dialect; making it reachable is a separate change — dbDialect flows into the project meta sent to the API, whose v1 dialect choices only know mysql.)
  • Anything outside postgres://, postgresql://, mysql://, mssql:// (SQL) and mongodb://, mongodb+srv:// (Mongo) — pg://, mysql2://, sqlite:// and friends all end up with no dialect, hence no driver in the generated project.

What is not rejected: uppercase schemes. getDialect() was matching them case-sensitively and returning null, which is what actually broke Postgres://…. Both Sequelize and @forestadmin/datasource-sql parse the URL with a URL parser that normalizes the scheme, so Postgres://user:MyPass@host/db connects and generates a working project — getDialect() is now case-insensitive (on a lowercased copy: the credentials the URL carries are case-sensitive), in src/services/projects/create/options.ts and in src/services/schema/update/database.js, which had the same bug on the schema:update path. The mongo validator is the exception and still requires lowercase, because the mongodb driver itself throws Invalid scheme, expected connection string to start with "mongodb://".

projects:create (v1, forest-express) is deliberately left untouched: adding a new hard failure to a legacy path costs more than the consistency is worth.

Error messages

Every rejection used to read "the scheme must be lowercase", including mariadb://, pg://, sqlite:// and a URL with a leading space. Each case now says what is actually wrong: unsupported scheme (listing the supported ones), URL truncated after ://, not a URL at all, and mariadb:// pointing at mysql://.

Tested

  • options.test.ts — the helpers: skip predicate (incl. blank), scheme validation per engine, each exact message, uppercase accepted on SQL and refused on Mongo, getDialect() case-insensitivity leaving the credentials alone, and a guard against a scheme resolving on Object.prototype.
  • connection-url-prompt.test.ts (new) — the command test helper stubs inquirer and returns canned answers, so nothing proved the field prompts were actually skipped. This one drives the real inquirer against the real command options and asserts which questions the user is asked (URL → fields skipped, blank → fields asked, nosql likewise), that the pasted credentials never reach the screen, and that the URL is stored trimmed.
  • database.unit.test.jsgetDialect() on the schema:update path, case-insensitive and non-destructive.
  • abstractProjectCreateCommand.unit.test.ts — a blank URL normalizes to undefined and the connection uses the field values; a URL pasted with whitespace is trimmed before the dialect is derived.
  • sql.test.js / nosql.test.js — interactive URL path (project created, dialect derived from the URL), field-flags path (no URL question in the prompt list), and the blank-URL round-trip against the test database.
  • tsc + lint clean.

🤖 Generated with Claude Code

Note

Add connection URL prompt to create:sql and create:nosql project commands

  • Adds a masked, validated connection URL prompt as the first option in both create:sql and create:nosql; when a URL is supplied, the individual database field prompts (host, port, user, password, name, dialect) are skipped
  • Adds shared validators (options.ts): validateSqlConnectionUrl accepts PostgreSQL, MySQL, and MSSQL schemes (rejects MariaDB with guidance), and validateMongoConnectionUrl accepts mongodb and mongodb+srv (requires lowercase scheme)
  • Adds trimConnectionUrl filter and skipWhenConnectionUrl predicate so blank URL answers fall back to the existing field prompts
  • Extends option-parser.optionToInquirer to treat explicitly secret options as masked password prompts and forward configured filters to Inquirer
  • Makes dialect detection case-insensitive in getDialect and Database.getDialect by lowercasing only the scheme comparison while preserving the original URL value
  • Behavioral Change: connection URLs with uppercase or mixed-case schemes now resolve correctly; MariaDB URLs are explicitly rejected in the SQL prompt with a message directing users to MySQL

Macroscope summarized 826d38c.

Comment thread src/services/projects/create/options.ts
Comment thread src/services/projects/create/options.ts Outdated
@Gawtier
Gawtier force-pushed the feat/create-prompt-connection-uri branch from 2518856 to 9222948 Compare July 26, 2026 15:12
…rompts

Interactive onboarding was field-only; the connection URL was reachable only via
the -c flag (prompter was null). This brings the CLI to parity with the web
onboarding (which has a Form/URL choice).

Scoped to the sql/nosql commands (no change to shared options, so init / create /
demo prompts are untouched): each overrides databaseConnectionURL with a leading
prompter ("leave blank to enter the details manually") and marks its field prompts
`when: skipWhenConnectionUrl` so they're skipped once a URL is given. sql validates
a SQL scheme, nosql a mongodb(+srv) scheme (rejects a bare `foo` and a cross-engine
URL). A blank answer is coerced to undefined so the fields path is used.

Tests: options.test.ts covers the helpers (scheme validation + skip predicate);
sql/nosql each get a command test of the interactive URL path (project created from
the URL, field prompts skipped). init/create prompt expectations are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Gawtier
Gawtier force-pushed the feat/create-prompt-connection-uri branch from 9222948 to 1e3a9ab Compare July 26, 2026 16:18
PMerlet and others added 2 commits July 31, 2026 11:59
Review fixes on the interactive connection-URL onboarding:

- skip the URL prompt when any database field flag is passed (exclusive
  list on the option, mirroring the field options): scripted/CI
  invocations that pass -d/-n/-h/-p/-u must not hang on a new question
- drop mariadb:// from the accepted SQL schemes: the mariadb driver is
  not shipped with generated projects, so Sequelize would throw after
  the project was already created server-side (use mysql:// instead)
- make scheme validation case-sensitive and say so in the error:
  getDialect() and the generated project match lowercase schemes only,
  so Postgres://… used to pass validation then silently produce a
  project with databaseType null and no driver
- test the blank-URL round-trip: empty answer falls back to the field
  prompts and the '' value is normalized to undefined for the DB config

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up on the optional connection URL prompt.

The URL embeds the database password, but the prompt was rendered as a plain
`input`: `optionToInquirer` only masks options whose *name* matches
/(password|secret)/, so the pasted credentials were echoed on screen and left in
the terminal scrollback — a regression against the field-by-field flow, whose
`databasePassword` prompt is masked. Options can now opt into masking explicitly
via `prompter.secret`, with a `*` mask so a failed paste stays distinguishable
from the blank answer that falls back to the fields.

A pasted URL routinely drags whitespace along. `'  postgres://…'` was rejected
with a message about lowercase schemes, and `'postgres://…  '` was accepted
verbatim and flowed into the connection test and the generated .env. Worse, the
untrimmed value reached `getDialect()` *during* prompting, so a leading space
made the database schema question disappear. Options can now declare a `filter`,
run by inquirer before `when`/`validate`; the URL trims itself at the source, and
`getCommandOptions` normalizes the `-c` flag path the same way.

Every rejection used to read "the scheme must be lowercase", including
`mariadb://`, `pg://`, `sqlite://` and a URL with a leading space. Each case now
says what is actually wrong, and mariadb:// points at mysql://.

Tested: the command test helper stubs inquirer and returns canned answers, so
nothing proved the field prompts were skipped. connection-url-prompt.test.ts
drives the real inquirer against the real command options and asserts which
questions are asked, that the credentials never reach the screen, and that the
blank answer still falls back to the field prompts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
// A blank answer to the optional connection URL prompt means "use the field prompts instead",
// and a pasted URL routinely carries surrounding whitespace. Normalize once, before the
// dialect is derived from it and before it reaches the connection test and the generated .env.
options.databaseConnectionURL = options.databaseConnectionURL?.trim() || undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/abstract-project-create-command.ts:223

Whitespace-only -c values still suppress all database field prompts, so this normalization leaves dbConfig without either a connection URL or dialect and the command exits with “Missing database dialect.” Normalize the value before getCommandLineOptions runs its interactive skip logic, or trim it in skipWhenConnectionUrl.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/abstract-project-create-command.ts around line 223:

Whitespace-only `-c` values still suppress all database field prompts, so this normalization leaves `dbConfig` without either a connection URL or dialect and the command exits with “Missing database dialect.” Normalize the value before `getCommandLineOptions` runs its interactive skip logic, or trim it in `skipWhenConnectionUrl`.

Review follow-up: shrink what the new connection URL validators reject.

Uppercase schemes were rejected because `getDialect()` matched them with a
case-sensitive `startsWith`, returned null, and left the generated project
without a driver in its package.json. But URL schemes are case-insensitive, and
both Sequelize and @forestadmin/datasource-sql parse the connection URL with a
URL parser that normalizes the scheme — verified: `new Sequelize('Postgres://…')`
yields the postgres dialect with the password untouched. So `Postgres://…` works
end to end, and the bug was ours. `getDialect()` now compares on a lowercased
copy (the credentials the URL carries are case-sensitive, and the URL itself is
handed to the driver untouched), here and on the schema:update path, which had
the same bug.

The SQL validator therefore accepts uppercase schemes, which cuts the breaking
change on the `-c` flag down to mariadb:// and genuinely unsupported schemes.

The mongo validator keeps requiring lowercase: unlike the SQL drivers, the
mongodb driver compares the scheme verbatim and throws "Invalid scheme, expected
connection string to start with mongodb://" on `MongoDB://…`. Rejecting it in
the prompt keeps that failure before the project is created on Forest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants