-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(client): Fix URL parsing in the CLI (#1296)
This is just to demonstrate the direction in which URL parsing can be improved in the CLI. First, more test coverage is needed for some not-too-uncommon edge cases. Second, more uniformunity is needed in the parsing code. Currently, in `src/cli/utils.ts` alone the database URL is parsed using a regexp, the service URL is parsed using the deprecated `url.parse()` method. All of this can be replaced with the built-in `URL` class and its properties/method. That class can also be used for building URLs, instead of ad-hoc string concatenation that doesn't escape URL components. --------- Co-authored-by: msfstef <msfstef@gmail.com>
- Loading branch information
Showing
17 changed files
with
508 additions
and
155 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"electric-sql": patch | ||
--- | ||
|
||
Consistently use `URL` API for parsing and constructing URLs in CLI. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"electric-sql": patch | ||
--- | ||
|
||
Ensure default port numbers are used when starting Electric with CLI. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
export * from './io' | ||
export * from './parse' | ||
export * from './string' | ||
export * from './paths' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import fs from 'fs' | ||
import { InvalidArgumentError } from 'commander' | ||
import { appPackageJsonPath } from './paths' | ||
|
||
/** | ||
* Get the name of the current project. | ||
*/ | ||
export function getAppName(): string | undefined { | ||
return JSON.parse(fs.readFileSync(appPackageJsonPath, 'utf8')).name | ||
} | ||
|
||
/** | ||
* Parse an integer from a string and throw the given error | ||
* if parsing fails | ||
*/ | ||
function parseIntOrFail(str: string, error: string) { | ||
const parsed = parseInt(str) | ||
if (isNaN(parsed)) { | ||
throw new InvalidArgumentError(error) | ||
} | ||
return parsed | ||
} | ||
|
||
export function parsePort(str: string): number { | ||
return parseIntOrFail( | ||
str, | ||
`Invalid port: ${str}. Must be integer between 1 and 65535.` | ||
) | ||
} | ||
|
||
export function parseTimeout(str: string): number { | ||
return parseIntOrFail(str, `Invalid timeout: ${str}. Must be an integer.`) | ||
} | ||
|
||
export function extractDatabaseURL(url: string): { | ||
user: string | ||
password: string | ||
host: string | ||
port: number | null | ||
dbName: string | ||
} { | ||
const parsed = new URL(url) | ||
if (!(parsed.protocol === 'postgres:' || parsed.protocol === 'postgresql:')) { | ||
throw new Error(`Invalid database URL scheme: ${url}`) | ||
} | ||
|
||
const user = decodeURIComponent(parsed.username) | ||
if (!user) { | ||
throw new Error(`Invalid or missing username: ${url}`) | ||
} | ||
|
||
return { | ||
user: user, | ||
password: decodeURIComponent(parsed.password), | ||
host: decodeURIComponent(parsed.hostname), | ||
port: parsed.port ? parseInt(parsed.port) : null, | ||
dbName: decodeURIComponent(parsed.pathname.slice(1)) || user, | ||
} | ||
} | ||
|
||
export function extractServiceURL(serviceUrl: string): { | ||
host: string | ||
port: number | null | ||
} { | ||
const parsed = new URL(serviceUrl) | ||
if (!parsed.hostname) { | ||
throw new Error(`Invalid service URL: ${serviceUrl}`) | ||
} | ||
return { | ||
host: decodeURIComponent(parsed.hostname), | ||
port: parsed.port ? parseInt(parsed.port) : null, | ||
} | ||
} | ||
|
||
/** | ||
* Parse the given string or number into a port number and whether | ||
* it uses the HTTP proxy or not. | ||
* @example | ||
* ``` | ||
* parsePgProxyPort('65432') // { http: false, port: 65432 } | ||
* parsePgProxyPort('http:5123') // { http: true, port: 5123 } | ||
* parsePgProxyPort('http') // { http: true, port: 65432 } | ||
* ``` | ||
*/ | ||
export function parsePgProxyPort( | ||
str: number | `${number}` | `http` | `http:${number}` | ||
): { | ||
http: boolean | ||
port: number | ||
} { | ||
if (typeof str === 'number') { | ||
return { http: false, port: str } | ||
} else if (str.includes(':')) { | ||
const [prefix, port] = str.split(':') | ||
return { | ||
http: prefix.toLocaleLowerCase() === 'http', | ||
port: parsePort(port), | ||
} | ||
} else if (str.toLocaleLowerCase() === 'http') { | ||
return { http: true, port: 65432 } | ||
} else { | ||
return { http: false, port: parsePort(str) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import path from 'path' | ||
|
||
/** | ||
* Path where the user ran `npx electric` | ||
*/ | ||
export const appRoot = path.resolve() | ||
|
||
/** | ||
* Path to the package.json of the user app | ||
*/ | ||
export const appPackageJsonPath = path.join(appRoot, 'package.json') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* Tagged template literal dedent function that also unwraps lines. | ||
* Double newlines become a single newline. | ||
*/ | ||
export function dedent( | ||
strings: TemplateStringsArray, | ||
...values: unknown[] | ||
): string { | ||
let str = strings[0] | ||
for (let i = 0; i < values.length; i++) { | ||
str += String(values[i]) + strings[i + 1] | ||
} | ||
|
||
const lines = str.split('\n') | ||
|
||
const minIndent = lines | ||
.filter((line) => line.trim()) | ||
.reduce((minIndent, line) => { | ||
const indent = line.match(/^\s*/)?.[0].length ?? 0 | ||
return indent < minIndent ? indent : minIndent | ||
}, Infinity) | ||
|
||
if (lines[0] === '') { | ||
// if first line is empty, remove it | ||
lines.shift() | ||
} | ||
if (lines[lines.length - 1] === '') { | ||
// if last line is empty, remove it | ||
lines.pop() | ||
} | ||
|
||
return lines | ||
.map((line) => { | ||
line = line.slice(minIndent) | ||
if (/^\s/.test(line)) { | ||
// if line starts with whitespace, prefix it with a newline | ||
// to preserve the indentation | ||
return '\n' + line | ||
} else if (line === '') { | ||
// if line is empty, we want a newline here | ||
return '\n' | ||
} else { | ||
return line.trim() + ' ' | ||
} | ||
}) | ||
.join('') | ||
.trim() | ||
} | ||
|
||
/** | ||
* Builds the Postgres database URL for the given parameters. | ||
*/ | ||
export function buildDatabaseURL(opts: { | ||
user: string | ||
password: string | ||
host: string | ||
port: number | ||
dbName: string | ||
ssl?: boolean | ||
}): string { | ||
const base = new URL(`postgresql://${opts.host}`) | ||
base.username = opts.user | ||
base.password = opts.password | ||
base.port = opts.port.toString() | ||
base.pathname = opts.dbName | ||
if (opts.ssl !== undefined) { | ||
base.searchParams.set('sslmode', opts.ssl ? 'require' : 'disable') | ||
} | ||
|
||
return base.toString() | ||
} |
Oops, something went wrong.