feat(connections): create a connection from a project folder (#1959)#1960
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27e9e7d6f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if directory == rootPath, rootDocument == nil || file.match.tier == .nearCertain { | ||
| rootDocument = document |
There was a problem hiding this comment.
Resolve configs from a deterministic environment file
When a project contains multiple root dotenv files, every .nearCertain file overwrites rootDocument; because the walker sorts paths, a common .env plus .env.production.local setup resolves Prisma, Rails, and Compose references using the production file simply because it sorts later. This can silently prefill production credentials, while the resulting candidate is attributed to the consuming config and may not trigger the production-path heuristic. Select the environment according to each format's precedence rather than allowing the last near-certain file to win.
Useful? React with 👍 / 👎.
| parsed.database.lowercased(), | ||
| parsed.username.lowercased(), | ||
| ].joined(separator: "\u{1F}") |
There was a problem hiding this comment.
Preserve database-specific fields in deduplication
When two candidates share type, host, port, database, and username but differ in a database-specific selector, this identity collapses distinct connections. For example, redis://host/0 and redis://host/1 differ only in redisDatabase, and Oracle services are stored in oracleServiceName while database is empty, so only one candidate survives. Include these fields, along with Mongo's multiHost, in the identity.
Useful? React with 👍 / 👎.
| if let candidate { | ||
| return candidate |
There was a problem hiding this comment.
Collect every distinct URL from a dotenv file
When one dotenv file defines more than one relational database, such as a PostgreSQL DATABASE_URL and a separate MYSQL_URL, firstURLCandidate returns after the first valid key and the second connection is never shown. The same early-return behavior also hides multiple Mongo or Redis URLs. Collect all valid keyed candidates and let the existing identity-based deduplication remove aliases that describe the same connection.
Useful? React with 👍 / 👎.
| while let open = remainder.range(of: "${") { | ||
| result += remainder[remainder.startIndex..<open.lowerBound] |
There was a problem hiding this comment.
Expand unbraced Compose variables
When Compose uses unbraced interpolation such as POSTGRES_PASSWORD=$DB_PASSWORD or a published port containing $DB_PORT, this loop never sees the variable because it searches only for ${. The scanner consequently imports the literal password $DB_PASSWORD or fails to recognize the published port, producing a connection that does not match the service. Implement $VAR interpolation and Compose's $$ escaping in addition to the braced form.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01a979e6f4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let parent = rootURL.standardizedFileURL.deletingLastPathComponent() | ||
| let candidateURL = parent.appendingPathComponent("wp-config.php") | ||
| let path = candidateURL.standardizedFileURL.path |
There was a problem hiding this comment.
Keep WordPress scanning inside the selected folder
When the selected folder has a sibling wp-config.php in its parent, this constructs and reads that file even though the user granted the scan only for rootURL. This violates the documented containment boundary and can load credentials from outside the chosen project; remove the parent lookup or require the user to select that parent explicitly.
Useful? React with 👍 / 👎.
| let prefix = variables["MARIADB_PASSWORD"] != nil || variables["MARIADB_DATABASE"] != nil | ||
| ? "MARIADB" | ||
| : "MYSQL" |
There was a problem hiding this comment.
Recognize MariaDB credentials from the service type
When a MariaDB Compose service defines only common variables such as MARIADB_ROOT_PASSWORD or MARIADB_USER, this condition selects the MYSQL prefix despite type == .mariadb. The resulting candidate silently loses the configured password/user and opens a connection form that cannot authenticate; select MARIADB from the database type or recognize every supported MariaDB variable.
Useful? React with 👍 / 👎.
| let sqlServerHints = ["trusted_connection", "encrypt", "trustservercertificate", "integrated security"] | ||
| if sqlServerHints.contains(where: { keywords[$0] != nil }) { | ||
| return .mssql | ||
| } | ||
| if keywords["uid"] != nil || keywords["pwd"] != nil { | ||
| return .mysql | ||
| } | ||
| return nil |
There was a problem hiding this comment.
Accept standard SQL Server connection strings using Database
When an appsettings entry uses the common SqlClient form Server=db;Database=app;User Id=sa;Password=..., none of these provider checks match because it uses neither Initial Catalog nor one of the listed SQL Server hints, and User Id/Password also do not satisfy the MySQL uid/pwd check. The connection is therefore omitted entirely even though all required SQL Server fields were parsed.
Useful? React with 👍 / 👎.
| guard let adapter = YamlMappingSupport.string(selected.mapping["adapter"]), | ||
| let type = adapterType(adapter) else { | ||
| return [] |
There was a problem hiding this comment.
Honor URL-based Rails database configurations
When a Rails environment is configured solely with url: <%= ENV["DATABASE_URL"] %>, which Rails supports, the selected mapping has no adapter and this guard discards it before resolving the URL. Such projects produce no Rails candidate even when the referenced value is available in the indexed dotenv file; derive the database type and fields from url when present.
Useful? React with 👍 / 👎.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45126ca585
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| Button(String(localized: "Open Project Folder...")) { | ||
| AppCommands.shared.openProjectFolder.send(()) |
There was a problem hiding this comment.
Route the menu action independently of the Welcome window
When a connection window is open, TabRouter.closeWelcomeWindows() has already closed the only view whose WelcomeViewModel subscribes to this subject and owns the project-scan sheet. Consequently, File > Open Project Folder... either has no live subscriber or updates sheet state belonging to a closed Welcome window, so the advertised menu route cannot display its results outside the Welcome screen. Route this command through an application-level coordinator or reopen the Welcome window before starting the flow.
Useful? React with 👍 / 👎.
| if lowercased.hasPrefix("jdbc:"), | ||
| !lowercased.hasPrefix("jdbc:sqlserver:"), | ||
| !lowercased.hasPrefix("jdbc:oracle:") { | ||
| value = String(value.dropFirst("jdbc:".count)) |
There was a problem hiding this comment.
Parse SQL Server and Oracle JDBC URL forms
When a Spring datasource uses standard database-specific JDBC syntax, these exclusions leave it in a form the downstream generic URL parser cannot handle: jdbc:sqlserver://localhost:1433;databaseName=app fails because the semicolon properties make the synthesized URLComponents authority invalid, while common Oracle forms such as jdbc:oracle:thin:@//host:1521/service fail the subsequent :// guard entirely. Such Spring projects silently produce no candidate; normalize or parse these JDBC formats explicitly.
Useful? React with 👍 / 👎.
| let separators: Set<Character> = ["=", ":"] | ||
| guard let index = line.firstIndex(where: { separators.contains($0) }) else { | ||
| return nil |
There was a problem hiding this comment.
Accept whitespace-separated Java properties
When a valid Java properties file uses whitespace as the key/value separator, for example spring.datasource.url jdbc:postgresql://localhost/app, this search finds neither = nor : and drops the assignment. Since whitespace separation is part of the .properties format, the scanner then misses the datasource URL—or separately loses its username and password—even though Spring accepts the file.
Useful? React with 👍 / 👎.
|
|
||
| static func parseKeywords(_ connectionString: String) -> [String: String] { | ||
| var keywords: [String: String] = [:] | ||
| for segment in connectionString.split(separator: ";") { |
There was a problem hiding this comment.
Honor quoting when splitting appsettings connection strings
When an ADO.NET connection-string value contains a semicolon inside quotes, such as Password="pa;ss", splitting blindly at every semicolon records the password as "pa and discards the remainder. The candidate is still presented as having a password but cannot authenticate, so the keyword parser needs to recognize quoted or escaped delimiters.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e459b601e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let turso = firstURLCandidate(context, keys: tursoURLKeys) { | ||
| candidates.append(turso) |
There was a problem hiding this comment.
Merge the Turso auth token into the candidate
When a dotenv file uses the standard TURSO_DATABASE_URL=libsql://... plus TURSO_AUTH_TOKEN=... pair, this path parses only the URL and leaves parsedURL.password empty. The connection form uses that password field for libSQL's API token, so the resulting Turso connection is prefilled without its credentials and fails authentication; merge TURSO_AUTH_TOKEN into the parsed candidate.
Useful? React with 👍 / 👎.
| let database = databaseKind(for: image) else { | ||
| return nil | ||
| } | ||
| let variables = environmentVariables(service["environment"]) |
There was a problem hiding this comment.
Load service credentials declared through env_file
When a Compose database service puts POSTGRES_*, MYSQL_*, or similar variables in env_file, this reads only the inline environment block. The candidate therefore gets blank or default credentials even though Compose supplies the referenced file at runtime; a separately scanned dotenv candidate also cannot reliably compensate because it lacks the service's published-port mapping. Resolve and merge the service's env_file entries before applying credentials.
Useful? React with 👍 / 👎.
| if name.contains("mongo") { | ||
| return ServiceDatabase(type: .mongodb, defaultPort: 27_017) |
There was a problem hiding this comment.
Exclude non-database images containing database names
When a Compose project includes a common sidecar such as mongo-express, this substring check classifies the admin UI as a MongoDB server and emits a bogus 127.0.0.1:27017 candidate. If the real Mongo service has credentials, its identity differs and deduplication retains both entries, leaving users able to select an unusable connection; match known database image repositories rather than any image containing mongo.
Useful? React with 👍 / 👎.
9d2bfca to
1c2b40d
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3e17659f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let isProduction = ScannedProductionHeuristic.isProduction( | ||
| relativePath: sourceRelativePath, | ||
| host: parsedURL.host, | ||
| database: parsedURL.database |
There was a problem hiding this comment.
Include the selected environment in production detection
When a Rails database.yml selects a production: section whose host and database lack a production marker (for example, db.example.com/app), this check ignores the sourceKey value production and leaves safeModeLevel unset. TransientConnectionFactory.build then defaults the imported connection to Silent rather than Alert, removing the intended production protection; include the selected environment/source key in the heuristic.
Useful? React with 👍 / 👎.
| if let nested = firstNestedDatabase(mapping) { | ||
| return ("\(name).\(nested.key)", nested.mapping) |
There was a problem hiding this comment.
Return every nested Rails database
When a Rails environment defines multiple databases such as development.primary and development.animals, this immediately returns the first nested mapping in alphabetical order, so every other connection is omitted and the chosen one may not even be primary. Extract a candidate for each nested mapping with an adapter instead of reducing the environment to one entry.
Useful? React with 👍 / 👎.
| private static func lastUserInfoSeparator(in remainder: String) -> String.Index? { | ||
| let boundary = remainder.firstIndex { $0 == "?" || $0 == "#" } ?? remainder.endIndex | ||
| let searchable = remainder[remainder.startIndex..<boundary] | ||
| return searchable.lastIndex(of: "@") |
There was a problem hiding this comment.
Stop treating path at-signs as credentials
When a credential-free URL contains a legal @ in its database/path, such as postgresql://localhost/my@db, this searches beyond the authority and treats that path character as the user-info separator. Normalization rewrites the value to postgresql://localhost%2Fmy@db, which is then parsed as host db with username localhost/my; limit user-info detection to the authority portion.
Useful? React with 👍 / 👎.
| let trimmed = entry.value.trimmingCharacters(in: .whitespaces) | ||
| return trimmed.isEmpty ? nil : trimmed |
There was a problem hiding this comment.
Preserve whitespace inside quoted dotenv values
When a quoted dotenv credential intentionally begins or ends with whitespace, this trims characters that the quoted syntax preserves; for example, DB_PASSWORD=" secret " is imported as secret, causing authentication to fail. Unquoted values are already trimmed by readUnquoted, so document lookup should retain the decoded value of quoted entries.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80ccaf8667
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case .mongodb: | ||
| fields.username = variables["MONGO_INITDB_ROOT_USERNAME"] ?? "" | ||
| fields.password = variables["MONGO_INITDB_ROOT_PASSWORD"] ?? "" | ||
| fields.database = variables["MONGO_INITDB_DATABASE"] ?? "" |
There was a problem hiding this comment.
Set the Mongo root authentication database to admin
When a Compose service supplies MONGO_INITDB_ROOT_USERNAME/MONGO_INITDB_ROOT_PASSWORD together with MONGO_INITDB_DATABASE=app, the official image creates the root user in admin, but this candidate sets database to app without setting authSource. MongoDBConnection consequently uses the nonempty application database as the effective authentication source, so the imported connection fails authentication; set authSource to admin for these root credentials.
Useful? React with 👍 / 👎.
| func environment(near url: URL) -> DotenvDocument? { | ||
| let directory = url.deletingLastPathComponent().standardizedFileURL.path | ||
| return documentsByDirectory[directory] ?? rootDocument |
There was a problem hiding this comment.
Search ancestor directories for the nearest dotenv file
When the selected folder is a monorepo and a nested application has apps/api/.env beside apps/api/config, a config such as apps/api/config/database.yml cannot resolve its ENV[...] fields: this lookup checks only the config file's exact directory and then jumps to the scan root, skipping apps/api/.env. That can produce a candidate with default host and blank credentials even though the relevant dotenv file was scanned; walk upward to the project root and use the nearest applicable document.
Useful? React with 👍 / 👎.
| let candidate = ScannedConnectionCandidate( | ||
| parsedURL: merged, | ||
| sourceRelativePath: relativePath, | ||
| sourceKey: "spring.datasource.url", | ||
| kind: .springYaml, | ||
| tier: .configFile | ||
| ) | ||
| return [candidate] |
There was a problem hiding this comment.
Return candidates from every Spring YAML document
When application.yml contains multiple --- documents with datasource URLs for different Spring profiles, this returns immediately after parsing the first document. Every later profile connection is silently omitted from the findings, even though loadDocuments loaded it; accumulate each parseable candidate and return after the loop instead.
Useful? React with 👍 / 👎.
Closes #1959.
Point TablePro at a project folder and it reads the database settings the project already has, instead of you copying them out of a
.envby hand.File > Open Project Folder...or the welcome-screen menu opens a folder chooser. TablePro scans the folder, lists what it found (destination, then type + file + key), and on Continue opens the connection form pre-filled. Nothing is saved or connected until you save it.Approach
This reuses the pipeline Import from URL already runs on:
Everything from
ParsedConnectionURLonward is untouched code that already handles every database type, including Mongo multi-host, Redis DB index, Oracle service names and SSH.ConnectionURLParserneeded no changes.Two shapes were considered and rejected:
ForeignAppImporter. Its contract is "is competitor app X installed, read its store" (appBundleIdentifier,installedAppURL(),readsPasswordsFromKeychain), none of which a folder has.ForeignAppImporterRegistryTestsalso hard-codescount == 6.ConnectionExportService.performImport. That exists for N-way batch import with duplicate resolution and post-import Keychain restore. This flow produces one pre-filled form, and going through that pipeline would write to storage before the user has seen the connection form.Files read
.envfamilyDATABASE_URL,MONGODB_URI,REDIS_URL,TURSO_DATABASE_URLand similar; LaravelDB_*; Postgres/MySQL/MariaDB/Mongo/SQL Server container variables;PG*wp-config.phpDB_NAME,DB_USER,DB_PASSWORD,DB_HOSTprisma/schema.prismadatasourceprovider and url, includingenv("...")config/database.yml<<merge keys, resolvingENV["..."]docker-compose.yml,compose.yamlapplication.properties,application.ymlspring.datasource.*appsettings.jsonConnectionStringsentriesTemplate files (
.env.example,.env.sample,.env.template,.env.dist) are skipped..envrcis a shell script, so it is never read or run. A project using more than one engine gets a row per engine; pooled and direct URLs for the same engine collapse to the direct one.Adds Yams 5.4.0 for the YAML formats.
The dotenv dialect
There is no dotenv spec and the real libraries disagree. Running 105 cases through motdotla, python-dotenv, phpdotenv, ruby-dotenv, godotenv, Node
--env-fileand Bun, they disagree on 57 of them.The parser targets the intersection plus a documented superset, and the choice is pinned by tests: inline
#requires preceding whitespace (sop#ssw0rdsurvives), double quotes expand\n \r \t \\ \" \'and leave unknown escapes intact, single quotes are fully literal,${VAR}/$VAR/${VAR:-default}resolve same-file then process env, last duplicate wins, one malformed line does not abort the file.Unresolvable indirection is reported rather than guessed, in both
.env(${{Postgres.DATABASE_URL}}) and Compose (${DB_PASSWORD}with nothing to resolve it).Security
The scanner reads files from repos the user may have just cloned, so containment is the design, not a checklist.
git init && ln -s /etc/passwd .env && git add -Astores a mode-120000 blob that a plaingit clonematerializes. Files are filtered onisRegularFile && !isSymbolicLinkbefore any read, and symlinked directories are never descended. Both cases have regression tests using real symlinks..standardizingPathonly.resolvingSymlinksInPath()andrealpath(3)rewrite/private/tmpand/tmpin opposite directions, and one returns its input unchanged for a nonexistent path, so a naive containment check passes vacuously.~/.ssh,~/.aws,~/.gnupg,~/.docker, Keychains,/etc) enforced even if that folder is picked directly..gitignoreis deliberately not honored, since it ignores exactly the files this feature targets.$(...)is blocked in the lexer rather than merely unwired, because ruby-dotenv genuinely runs it through a subshell.PasswordSourceis ever synthesized from scanned content, sincePasswordSource.commandruns/bin/bash -c.Reads stay inside the chosen folder with one deliberate exception: WordPress permits
wp-config.phpone level above the site, so that single filename is probed in the parent and labelled../wp-config.phpin the UI. This is documented.A connection whose source file or host is named for production starts at Safe Mode Alert rather than Silent, reusing the existing
SafeModeLevelmachinery. The marker match is token-based, soproduction_orderscounts andproducts.example.comdoes not.UI
The findings list follows Apple's documented row anatomy rather than a label/value grid: leading type icon, destination as the title (middle-truncated, file paths shown relative to the project root),
Type · file · keyas the subtitle, and trailing SF Symbols for status with.help()plus.accessibilityLabel..listStyle(.bordered)is the macOS-only dialog list style. Full sentences for the selected row go in a detail strip under the list, which is HIG's prescribed escape hatch for content that does not fit a row.Passwords are never rendered. A row shows a key symbol and the value goes straight to the Keychain on save, per HIG's "Never prepopulate a password field".
Also in this PR
Not part of #1959, but a consequence of it or found while testing it:
Try Sample Databasemoved to the connection list, which already offers it when empty.Import from URL...added to the File menu. It previously existed nowhere in the menu bar, reachable only from the type-chooser sheet footer.Import from Other App...,Open Project Folder...,Import Connections...andExport Connections...posted toAppCommandssubjects onlyWelcomeViewModelobserves, so with the welcome window hidden they silently did nothing.+tooltip no longer hard-codes ⌘N. New Connection resolves through the user-remappable keyboard map, so the tooltip could lie; it now derives fromBoundKey.displayString. The ⌘N / ⌘, hint chips were removed for the same reason plus lack of any HIG basis for key equivalents outside menus.onAppearfired, but AppKit threwNSInternalInconsistencyExceptionfrom-[NSRemoteView containingWindowWillOrderOnScreen:]while ordering the sheet window, so it never became visible; afterwards the state stuck and further clicks were no-ops. Cause was forcing focus into a text field during sheet presentation, which spins up AutoFill's remote view mid-ordering.GeneralPaneViewwas left alone, since it is aFormin a window rather than a sheet.Note for future readers: the assertion above is thrown inside Apple's
ViewBridgefrom anAppleInternalbuild path and may be a beta-OS issue. Removing the forced focus avoids provoking it; please do not restore those.defaultFocuscalls without rechecking.Tests
~100 tests, all passing, plus the existing
ConnectionURLParserTestsstill green.Bugs caught by tests and fixed in source rather than worked around:
${POSTGRES_PASSWORD:-!ChangeMe!}substituted!ChangeMe!into YAML where!starts a tag, and the file failed to parse. Docker does interpolation post-parse; now so does this.ConnectionURLParserreturnsport == nilfor a default port, so.envandappsettings.jsondescribing the same database did not match. Identity now resolves throughtype.defaultPort.@userinfo split was applied to file-based URLs too, sosqlite:///Users/me/@scope/db.sqlitebecamesqlite://%2FUsers%2Fme%2F@scope/db.sqliteand pointed at a nonexistent file. It now skips any URL whose remainder begins with/, which also protects Postgres's unix-socket form.NSOpenPanelis not scriptable in CI, so there is no UI automation for the picker itself, consistent with the existing Import from Other App flow. Every piece of logic behind it is covered at unit and integration level.Notes
swiftlint --strictclean on all touched files.TablePro/Resources/Localizable.xcstringsis deliberately not in this branch. It already had unrelated pending extractions in the working tree, and Xcode regenerates it on build.docs/features/project-folder-import.mdx, in the Workflow group, cross-linked from the connection URL page.