mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
FEATURE (parsing): Add parsing connection string on DB creation
This commit is contained in:
5
.github/workflows/ci-release.yml
vendored
5
.github/workflows/ci-release.yml
vendored
@@ -82,6 +82,11 @@ jobs:
|
||||
cd frontend
|
||||
npm run lint
|
||||
|
||||
- name: Run frontend tests
|
||||
run: |
|
||||
cd frontend
|
||||
npm run test
|
||||
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-backend]
|
||||
|
||||
1092
frontend/package-lock.json
generated
1092
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
@@ -22,6 +24,7 @@
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/react": "^19.1.2",
|
||||
@@ -36,6 +39,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
ConnectionStringParser,
|
||||
type ParseError,
|
||||
type ParseResult,
|
||||
} from './ConnectionStringParser';
|
||||
|
||||
describe('ConnectionStringParser', () => {
|
||||
// Helper to assert successful parse
|
||||
const expectSuccess = (result: ParseResult | ParseError): ParseResult => {
|
||||
expect('error' in result).toBe(false);
|
||||
return result as ParseResult;
|
||||
};
|
||||
|
||||
// Helper to assert parse error
|
||||
const expectError = (result: ParseResult | ParseError): ParseError => {
|
||||
expect('error' in result).toBe(true);
|
||||
return result as ParseError;
|
||||
};
|
||||
|
||||
describe('Standard PostgreSQL URI (postgresql://)', () => {
|
||||
it('should parse basic postgresql:// connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://myuser:mypassword@localhost:5432/mydb'),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('localhost');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('myuser');
|
||||
expect(result.password).toBe('mypassword');
|
||||
expect(result.database).toBe('mydb');
|
||||
expect(result.isHttps).toBe(false);
|
||||
});
|
||||
|
||||
it('should default port to 5432 when not specified', () => {
|
||||
const result = expectSuccess(ConnectionStringParser.parse('postgresql://user:pass@host/db'));
|
||||
|
||||
expect(result.port).toBe(5432);
|
||||
});
|
||||
|
||||
it('should handle URL-encoded passwords', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://user:p%40ss%23word@host:5432/db'),
|
||||
);
|
||||
|
||||
expect(result.password).toBe('p@ss#word');
|
||||
});
|
||||
|
||||
it('should handle URL-encoded usernames', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://user%40domain:password@host:5432/db'),
|
||||
);
|
||||
|
||||
expect(result.username).toBe('user@domain');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Postgres URI (postgres://)', () => {
|
||||
it('should parse basic postgres:// connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgres://admin:secret@db.example.com:5432/production'),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('db.example.com');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('admin');
|
||||
expect(result.password).toBe('secret');
|
||||
expect(result.database).toBe('production');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Supabase Direct Connection', () => {
|
||||
it('should parse Supabase direct connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://postgres:mySecretPassword@db.abcdefghijklmnop.supabase.co:5432/postgres',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('db.abcdefghijklmnop.supabase.co');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('postgres');
|
||||
expect(result.password).toBe('mySecretPassword');
|
||||
expect(result.database).toBe('postgres');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Supabase Pooler Connection', () => {
|
||||
it('should parse Supabase pooler session mode connection string (port 5432)', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgres://postgres.abcdefghijklmnop:myPassword@aws-0-us-east-1.pooler.supabase.com:5432/postgres',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('aws-0-us-east-1.pooler.supabase.com');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('postgres.abcdefghijklmnop');
|
||||
expect(result.password).toBe('myPassword');
|
||||
expect(result.database).toBe('postgres');
|
||||
});
|
||||
|
||||
it('should parse Supabase pooler transaction mode connection string (port 6543)', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgres://postgres.projectref:myPassword@aws-0-eu-west-1.pooler.supabase.com:6543/postgres',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('aws-0-eu-west-1.pooler.supabase.com');
|
||||
expect(result.port).toBe(6543);
|
||||
expect(result.username).toBe('postgres.projectref');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JDBC Connection String', () => {
|
||||
it('should parse JDBC connection string with user and password params', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'jdbc:postgresql://localhost:5432/mydb?user=admin&password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('localhost');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('admin');
|
||||
expect(result.password).toBe('secret');
|
||||
expect(result.database).toBe('mydb');
|
||||
});
|
||||
|
||||
it('should parse JDBC connection string without port', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'jdbc:postgresql://db.example.com/mydb?user=admin&password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('db.example.com');
|
||||
expect(result.port).toBe(5432);
|
||||
});
|
||||
|
||||
it('should parse JDBC with sslmode parameter', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'jdbc:postgresql://host:5432/db?user=u&password=p&sslmode=require',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error for JDBC without user parameter', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('jdbc:postgresql://host:5432/db?password=secret'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('user');
|
||||
expect(result.format).toBe('JDBC');
|
||||
});
|
||||
|
||||
it('should return error for JDBC without password parameter', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('jdbc:postgresql://host:5432/db?user=admin'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('Password');
|
||||
expect(result.format).toBe('JDBC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Neon Connection String', () => {
|
||||
it('should parse Neon connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://neonuser:password123@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('ep-cool-name-123456.us-east-2.aws.neon.tech');
|
||||
expect(result.username).toBe('neonuser');
|
||||
expect(result.database).toBe('neondb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Railway Connection String', () => {
|
||||
it('should parse Railway connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://postgres:railwaypass@containers-us-west-123.railway.app:5432/railway',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('containers-us-west-123.railway.app');
|
||||
expect(result.username).toBe('postgres');
|
||||
expect(result.database).toBe('railway');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Render Connection String', () => {
|
||||
it('should parse Render connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://renderuser:renderpass@dpg-abc123.oregon-postgres.render.com/mydb',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('dpg-abc123.oregon-postgres.render.com');
|
||||
expect(result.username).toBe('renderuser');
|
||||
expect(result.database).toBe('mydb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DigitalOcean Connection String', () => {
|
||||
it('should parse DigitalOcean connection string with sslmode', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://doadmin:dopassword@db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com:25060/defaultdb?sslmode=require',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com');
|
||||
expect(result.port).toBe(25060);
|
||||
expect(result.username).toBe('doadmin');
|
||||
expect(result.database).toBe('defaultdb');
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AWS RDS Connection String', () => {
|
||||
it('should parse AWS RDS connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://rdsuser:rdspass@mydb.abc123xyz.us-east-1.rds.amazonaws.com:5432/mydb',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('mydb.abc123xyz.us-east-1.rds.amazonaws.com');
|
||||
expect(result.username).toBe('rdsuser');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Azure Database for PostgreSQL Connection String', () => {
|
||||
it('should parse Azure connection string with user@server format', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://myuser@myserver:mypassword@myserver.postgres.database.azure.com:5432/mydb?sslmode=require',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('myserver.postgres.database.azure.com');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('myuser');
|
||||
expect(result.password).toBe('mypassword');
|
||||
expect(result.database).toBe('mydb');
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Heroku Connection String', () => {
|
||||
it('should parse Heroku connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgres://herokuuser:herokupass@ec2-12-34-56-789.compute-1.amazonaws.com:5432/herokudb',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('ec2-12-34-56-789.compute-1.amazonaws.com');
|
||||
expect(result.username).toBe('herokuuser');
|
||||
expect(result.database).toBe('herokudb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CockroachDB Connection String', () => {
|
||||
it('should parse CockroachDB connection string with sslmode=verify-full', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://crdbuser:crdbpass@free-tier.gcp-us-central1.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('free-tier.gcp-us-central1.cockroachlabs.cloud');
|
||||
expect(result.port).toBe(26257);
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSL Mode Handling', () => {
|
||||
it('should set isHttps=true for sslmode=require', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=require'),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isHttps=true for sslmode=verify-ca', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=verify-ca'),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isHttps=true for sslmode=verify-full', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=verify-full'),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isHttps=false for sslmode=disable', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=disable'),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(false);
|
||||
});
|
||||
|
||||
it('should set isHttps=false when no sslmode specified', () => {
|
||||
const result = expectSuccess(ConnectionStringParser.parse('postgresql://u:p@host:5432/db'));
|
||||
|
||||
expect(result.isHttps).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('libpq Key-Value Format', () => {
|
||||
it('should parse libpq format connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'host=localhost port=5432 dbname=mydb user=admin password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('localhost');
|
||||
expect(result.port).toBe(5432);
|
||||
expect(result.username).toBe('admin');
|
||||
expect(result.password).toBe('secret');
|
||||
expect(result.database).toBe('mydb');
|
||||
});
|
||||
|
||||
it('should parse libpq format with quoted password containing spaces', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
"host=localhost port=5432 dbname=mydb user=admin password='my secret pass'",
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.password).toBe('my secret pass');
|
||||
});
|
||||
|
||||
it('should default port to 5432 when not specified in libpq format', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('host=localhost dbname=mydb user=admin password=secret'),
|
||||
);
|
||||
|
||||
expect(result.port).toBe(5432);
|
||||
});
|
||||
|
||||
it('should handle hostaddr as alternative to host', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'hostaddr=192.168.1.1 port=5432 dbname=mydb user=admin password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('192.168.1.1');
|
||||
});
|
||||
|
||||
it('should handle database as alternative to dbname', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'host=localhost port=5432 database=mydb user=admin password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.database).toBe('mydb');
|
||||
});
|
||||
|
||||
it('should handle username as alternative to user', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'host=localhost port=5432 dbname=mydb username=admin password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.username).toBe('admin');
|
||||
});
|
||||
|
||||
it('should parse sslmode in libpq format', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'host=localhost dbname=mydb user=admin password=secret sslmode=require',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error for libpq format missing host', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('port=5432 dbname=mydb user=admin password=secret'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('Host');
|
||||
expect(result.format).toBe('libpq');
|
||||
});
|
||||
|
||||
it('should return error for libpq format missing user', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('host=localhost dbname=mydb password=secret'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('Username');
|
||||
expect(result.format).toBe('libpq');
|
||||
});
|
||||
|
||||
it('should return error for libpq format missing password', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('host=localhost dbname=mydb user=admin'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('Password');
|
||||
expect(result.format).toBe('libpq');
|
||||
});
|
||||
|
||||
it('should return error for libpq format missing dbname', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('host=localhost user=admin password=secret'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('Database');
|
||||
expect(result.format).toBe('libpq');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Cases', () => {
|
||||
it('should return error for empty string', () => {
|
||||
const result = expectError(ConnectionStringParser.parse(''));
|
||||
|
||||
expect(result.error).toContain('empty');
|
||||
});
|
||||
|
||||
it('should return error for whitespace-only string', () => {
|
||||
const result = expectError(ConnectionStringParser.parse(' '));
|
||||
|
||||
expect(result.error).toContain('empty');
|
||||
});
|
||||
|
||||
it('should return error for unrecognized format', () => {
|
||||
const result = expectError(ConnectionStringParser.parse('some random text'));
|
||||
|
||||
expect(result.error).toContain('Unrecognized');
|
||||
});
|
||||
|
||||
it('should return error for missing username in URI', () => {
|
||||
const result = expectError(
|
||||
ConnectionStringParser.parse('postgresql://:password@host:5432/db'),
|
||||
);
|
||||
|
||||
expect(result.error).toContain('Username');
|
||||
});
|
||||
|
||||
it('should return error for missing password in URI', () => {
|
||||
const result = expectError(ConnectionStringParser.parse('postgresql://user@host:5432/db'));
|
||||
|
||||
expect(result.error).toContain('Password');
|
||||
});
|
||||
|
||||
it('should return error for missing database in URI', () => {
|
||||
const result = expectError(ConnectionStringParser.parse('postgresql://user:pass@host:5432/'));
|
||||
|
||||
expect(result.error).toContain('Database');
|
||||
});
|
||||
|
||||
it('should return error for invalid JDBC format', () => {
|
||||
const result = expectError(ConnectionStringParser.parse('jdbc:postgresql://invalid'));
|
||||
|
||||
expect(result.format).toBe('JDBC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle special characters in password', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://user:p%40ss%3Aw%2Ford@host:5432/db'),
|
||||
);
|
||||
|
||||
expect(result.password).toBe('p@ss:w/ord');
|
||||
});
|
||||
|
||||
it('should handle numeric database names', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://user:pass@host:5432/12345'),
|
||||
);
|
||||
|
||||
expect(result.database).toBe('12345');
|
||||
});
|
||||
|
||||
it('should handle hyphenated host names', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse('postgresql://user:pass@my-database-host.example.com:5432/db'),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('my-database-host.example.com');
|
||||
});
|
||||
|
||||
it('should handle connection string with extra query parameters', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(
|
||||
'postgresql://user:pass@host:5432/db?sslmode=require&connect_timeout=10&application_name=myapp',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.isHttps).toBe(true);
|
||||
expect(result.database).toBe('db');
|
||||
});
|
||||
|
||||
it('should trim whitespace from connection string', () => {
|
||||
const result = expectSuccess(
|
||||
ConnectionStringParser.parse(' postgresql://user:pass@host:5432/db '),
|
||||
);
|
||||
|
||||
expect(result.host).toBe('host');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
export type ParseResult = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
isHttps: boolean;
|
||||
};
|
||||
|
||||
export type ParseError = {
|
||||
error: string;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
export class ConnectionStringParser {
|
||||
/**
|
||||
* Parses a PostgreSQL connection string in various formats.
|
||||
*
|
||||
* Supported formats:
|
||||
* 1. Standard PostgreSQL URI: postgresql://user:pass@host:port/db
|
||||
* 2. Postgres URI: postgres://user:pass@host:port/db
|
||||
* 3. Supabase Direct: postgresql://postgres:pass@db.xxx.supabase.co:5432/postgres
|
||||
* 4. Supabase Pooler Session: postgres://postgres.ref:pass@aws-0-region.pooler.supabase.com:5432/postgres
|
||||
* 5. Supabase Pooler Transaction: same as above with port 6543
|
||||
* 6. JDBC: jdbc:postgresql://host:port/db?user=x&password=y
|
||||
* 7. Neon: postgresql://user:pass@ep-xxx.neon.tech/db
|
||||
* 8. Railway: postgresql://postgres:pass@xxx.railway.app:port/railway
|
||||
* 9. Render: postgresql://user:pass@xxx.render.com/db
|
||||
* 10. DigitalOcean: postgresql://user:pass@xxx.ondigitalocean.com:port/db?sslmode=require
|
||||
* 11. AWS RDS: postgresql://user:pass@xxx.rds.amazonaws.com:port/db
|
||||
* 12. Azure: postgresql://user@server:pass@xxx.postgres.database.azure.com:port/db?sslmode=require
|
||||
* 13. Heroku: postgres://user:pass@ec2-xxx.amazonaws.com:port/db
|
||||
* 14. CockroachDB: postgresql://user:pass@xxx.cockroachlabs.cloud:port/db?sslmode=verify-full
|
||||
* 15. With SSL params: postgresql://user:pass@host:port/db?sslmode=require
|
||||
* 16. libpq key-value: host=x port=5432 dbname=db user=u password=p
|
||||
*/
|
||||
static parse(connectionString: string): ParseResult | ParseError {
|
||||
const trimmed = connectionString.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return { error: 'Connection string is empty' };
|
||||
}
|
||||
|
||||
// Try JDBC format first (starts with jdbc:)
|
||||
if (trimmed.startsWith('jdbc:postgresql://')) {
|
||||
return this.parseJdbc(trimmed);
|
||||
}
|
||||
|
||||
// Try libpq key-value format (contains key=value pairs without ://)
|
||||
if (this.isLibpqFormat(trimmed)) {
|
||||
return this.parseLibpq(trimmed);
|
||||
}
|
||||
|
||||
// Try URI format (postgresql:// or postgres://)
|
||||
if (trimmed.startsWith('postgresql://') || trimmed.startsWith('postgres://')) {
|
||||
return this.parseUri(trimmed);
|
||||
}
|
||||
|
||||
return {
|
||||
error: 'Unrecognized connection string format',
|
||||
};
|
||||
}
|
||||
|
||||
private static isLibpqFormat(str: string): boolean {
|
||||
// libpq format has key=value pairs separated by spaces
|
||||
// Must contain at least host= or dbname= to be considered libpq format
|
||||
return (
|
||||
!str.includes('://') &&
|
||||
(str.includes('host=') || str.includes('dbname=')) &&
|
||||
str.includes('=')
|
||||
);
|
||||
}
|
||||
|
||||
private static parseUri(connectionString: string): ParseResult | ParseError {
|
||||
try {
|
||||
// Handle Azure format where username contains @: user@server:pass
|
||||
// Azure format: postgresql://user@servername:password@host:port/db
|
||||
const azureMatch = connectionString.match(
|
||||
/^postgres(?:ql)?:\/\/([^@:]+)@([^:]+):([^@]+)@([^:/?]+):?(\d+)?\/([^?]+)(?:\?(.*))?$/,
|
||||
);
|
||||
|
||||
if (azureMatch) {
|
||||
const [, user, , password, host, port, database, queryString] = azureMatch;
|
||||
const isHttps = this.checkSslMode(queryString);
|
||||
|
||||
return {
|
||||
host: host,
|
||||
port: port ? parseInt(port, 10) : 5432,
|
||||
username: decodeURIComponent(user),
|
||||
password: decodeURIComponent(password),
|
||||
database: decodeURIComponent(database),
|
||||
isHttps,
|
||||
};
|
||||
}
|
||||
|
||||
// Standard URI parsing using URL API
|
||||
const url = new URL(connectionString);
|
||||
|
||||
const host = url.hostname;
|
||||
const port = url.port ? parseInt(url.port, 10) : 5432;
|
||||
const username = decodeURIComponent(url.username);
|
||||
const password = decodeURIComponent(url.password);
|
||||
const database = decodeURIComponent(url.pathname.slice(1)); // Remove leading /
|
||||
const isHttps = this.checkSslMode(url.search);
|
||||
|
||||
// Validate required fields
|
||||
if (!host) {
|
||||
return { error: 'Host is missing from connection string' };
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
return { error: 'Username is missing from connection string' };
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return { error: 'Password is missing from connection string' };
|
||||
}
|
||||
|
||||
if (!database) {
|
||||
return { error: 'Database name is missing from connection string' };
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
database,
|
||||
isHttps,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: `Failed to parse connection string: ${(e as Error).message}`,
|
||||
format: 'URI',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static parseJdbc(connectionString: string): ParseResult | ParseError {
|
||||
try {
|
||||
// JDBC format: jdbc:postgresql://host:port/database?user=x&password=y
|
||||
const jdbcRegex = /^jdbc:postgresql:\/\/([^:/?]+):?(\d+)?\/([^?]+)(?:\?(.*))?$/;
|
||||
const match = connectionString.match(jdbcRegex);
|
||||
|
||||
if (!match) {
|
||||
return {
|
||||
error:
|
||||
'Invalid JDBC connection string format. Expected: jdbc:postgresql://host:port/database?user=x&password=y',
|
||||
format: 'JDBC',
|
||||
};
|
||||
}
|
||||
|
||||
const [, host, port, database, queryString] = match;
|
||||
|
||||
if (!queryString) {
|
||||
return {
|
||||
error: 'JDBC connection string is missing query parameters (user and password)',
|
||||
format: 'JDBC',
|
||||
};
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(queryString);
|
||||
const username = params.get('user');
|
||||
const password = params.get('password');
|
||||
const isHttps = this.checkSslMode(queryString);
|
||||
|
||||
if (!username) {
|
||||
return {
|
||||
error: 'Username (user parameter) is missing from JDBC connection string',
|
||||
format: 'JDBC',
|
||||
};
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return {
|
||||
error: 'Password parameter is missing from JDBC connection string',
|
||||
format: 'JDBC',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
port: port ? parseInt(port, 10) : 5432,
|
||||
username: decodeURIComponent(username),
|
||||
password: decodeURIComponent(password),
|
||||
database: decodeURIComponent(database),
|
||||
isHttps,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: `Failed to parse JDBC connection string: ${(e as Error).message}`,
|
||||
format: 'JDBC',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static parseLibpq(connectionString: string): ParseResult | ParseError {
|
||||
try {
|
||||
// libpq format: host=x port=5432 dbname=db user=u password=p
|
||||
// Values can be quoted with single quotes: password='my pass'
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
// Match key=value or key='quoted value'
|
||||
const regex = /(\w+)=(?:'([^']*)'|(\S+))/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(connectionString)) !== null) {
|
||||
const key = match[1];
|
||||
const value = match[2] !== undefined ? match[2] : match[3];
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
const host = params['host'] || params['hostaddr'];
|
||||
const port = params['port'];
|
||||
const database = params['dbname'] || params['database'];
|
||||
const username = params['user'] || params['username'];
|
||||
const password = params['password'];
|
||||
const sslmode = params['sslmode'];
|
||||
|
||||
if (!host) {
|
||||
return {
|
||||
error: 'Host is missing from connection string. Use host=hostname',
|
||||
format: 'libpq',
|
||||
};
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
return {
|
||||
error: 'Username is missing from connection string. Use user=username',
|
||||
format: 'libpq',
|
||||
};
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return {
|
||||
error: 'Password is missing from connection string. Use password=yourpassword',
|
||||
format: 'libpq',
|
||||
};
|
||||
}
|
||||
|
||||
if (!database) {
|
||||
return {
|
||||
error: 'Database name is missing from connection string. Use dbname=database',
|
||||
format: 'libpq',
|
||||
};
|
||||
}
|
||||
|
||||
const isHttps = this.isSslEnabled(sslmode);
|
||||
|
||||
return {
|
||||
host,
|
||||
port: port ? parseInt(port, 10) : 5432,
|
||||
username,
|
||||
password,
|
||||
database,
|
||||
isHttps,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: `Failed to parse libpq connection string: ${(e as Error).message}`,
|
||||
format: 'libpq',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static checkSslMode(queryString: string | undefined | null): boolean {
|
||||
if (!queryString) return false;
|
||||
|
||||
const params = new URLSearchParams(
|
||||
queryString.startsWith('?') ? queryString.slice(1) : queryString,
|
||||
);
|
||||
const sslmode = params.get('sslmode');
|
||||
|
||||
return this.isSslEnabled(sslmode);
|
||||
}
|
||||
|
||||
private static isSslEnabled(sslmode: string | null | undefined): boolean {
|
||||
if (!sslmode) return false;
|
||||
|
||||
// These modes require SSL
|
||||
const sslModes = ['require', 'verify-ca', 'verify-full'];
|
||||
return sslModes.includes(sslmode.toLowerCase());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, InputNumber, Select, Switch } from 'antd';
|
||||
import { CopyOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { App, Button, Input, InputNumber, Select, Switch } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { type Database, DatabaseType, databaseApi } from '../../../../entity/databases';
|
||||
import { ConnectionStringParser } from '../../../../entity/databases/model/postgresql/ConnectionStringParser';
|
||||
import { ToastHelper } from '../../../../shared/toast';
|
||||
|
||||
interface Props {
|
||||
@@ -35,6 +36,8 @@ export const EditDatabaseSpecificDataComponent = ({
|
||||
onSaved,
|
||||
isShowDbName = true,
|
||||
}: Props) => {
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [editingDatabase, setEditingDatabase] = useState<Database>();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
@@ -47,6 +50,46 @@ export const EditDatabaseSpecificDataComponent = ({
|
||||
|
||||
const [hasAutoAddedPublicSchema, setHasAutoAddedPublicSchema] = useState(false);
|
||||
|
||||
const parseFromClipboard = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
message.error('Clipboard is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = ConnectionStringParser.parse(trimmedText);
|
||||
|
||||
if ('error' in result) {
|
||||
message.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editingDatabase?.postgresql) return;
|
||||
|
||||
const updatedDatabase: Database = {
|
||||
...editingDatabase,
|
||||
postgresql: {
|
||||
...editingDatabase.postgresql,
|
||||
host: result.host,
|
||||
port: result.port,
|
||||
username: result.username,
|
||||
password: result.password,
|
||||
database: result.database,
|
||||
isHttps: result.isHttps,
|
||||
},
|
||||
};
|
||||
|
||||
setEditingDatabase(autoAddPublicSchemaForSupabase(updatedDatabase));
|
||||
setIsConnectionTested(false);
|
||||
message.success('Connection string parsed successfully');
|
||||
} catch {
|
||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||
}
|
||||
};
|
||||
|
||||
const autoAddPublicSchemaForSupabase = (updatedDatabase: Database): Database => {
|
||||
if (hasAutoAddedPublicSchema) return updatedDatabase;
|
||||
|
||||
@@ -140,6 +183,17 @@ export const EditDatabaseSpecificDataComponent = ({
|
||||
<div>
|
||||
{editingDatabase.type === DatabaseType.POSTGRES && (
|
||||
<>
|
||||
<div className="mb-3 flex">
|
||||
<div className="min-w-[150px]" />
|
||||
<div
|
||||
className="cursor-pointer text-sm text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
onClick={parseFromClipboard}
|
||||
>
|
||||
<CopyOutlined className="mr-1" />
|
||||
Parse from clipboard
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[150px]">Host</div>
|
||||
<Input
|
||||
|
||||
8
frontend/vitest.config.ts
Normal file
8
frontend/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
470
package-lock.json
generated
470
package-lock.json
generated
@@ -1,470 +0,0 @@
|
||||
{
|
||||
"name": "postgresus",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@types/recharts": "^1.8.29",
|
||||
"recharts": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^10.0.3",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
|
||||
"integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "1.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
|
||||
"integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
|
||||
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/recharts": {
|
||||
"version": "1.8.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz",
|
||||
"integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-shape": "^1",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.10",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||
"integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
|
||||
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.0.tgz",
|
||||
"integrity": "sha512-fX0xCgNXo6mag9wz3oLuANR+dUQM4uIlTYBGTGq9CBRgW/8TZPzqPGYs5NTt8aENCf+i1CI8vqxT1py8L/5J2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor/node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@types/recharts": "^1.8.29",
|
||||
"recharts": "^3.2.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user