mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ca38f5583 | ||
|
|
40b3ff61c7 | ||
|
|
e1b245a573 | ||
|
|
fdf29b71f2 | ||
|
|
49da981c21 | ||
|
|
9d611d3559 | ||
|
|
22cab53dab | ||
|
|
d761c4156c | ||
|
|
cbb8b82711 | ||
|
|
8e3d1e5bff | ||
|
|
349e7f0ee8 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug or unexpected behavior in Databasus
|
||||
labels: bug
|
||||
---
|
||||
|
||||
## Databasus version
|
||||
|
||||
<!-- e.g. 1.4.2 -->
|
||||
|
||||
## Operating system and architecture
|
||||
|
||||
<!-- e.g. Ubuntu 22.04 x64, macOS 14 ARM, Windows 11 x64 -->
|
||||
|
||||
## Describe the bug (please write manually, do not ask AI to summarize)
|
||||
|
||||
**What happened:**
|
||||
|
||||
**What I expected:**
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Have you asked AI how to solve the issue?
|
||||
|
||||
<!-- Using AI to diagnose issues before filing a bug report helps narrow down root causes. -->
|
||||
|
||||
- [ ] Claude Sonnet 4.6 or newer
|
||||
- [ ] ChatGPT 5.2 or newer
|
||||
- [ ] No
|
||||
|
||||
|
||||
## Additional context / logs
|
||||
|
||||
<!-- Screenshots, error messages, relevant log output, etc. -->
|
||||
@@ -55,7 +55,7 @@
|
||||
- **Time period**: Keep backups for a fixed duration (e.g., 7 days, 3 months, 1 year)
|
||||
- **Count**: Keep a fixed number of the most recent backups (e.g., last 30)
|
||||
- **GFS (Grandfather-Father-Son)**: Layered retention — keep hourly, daily, weekly, monthly and yearly backups independently for fine-grained long-term history (enterprises requirement)
|
||||
- **Size limits**: Set per-backup and total storage size caps to control storage гыфпу
|
||||
- **Size limits**: Set per-backup and total storage size caps to control storage usage
|
||||
|
||||
### 🗄️ **Multiple storage destinations** <a href="https://databasus.com/storages">(view supported)</a>
|
||||
|
||||
|
||||
@@ -25,15 +25,16 @@ type MongodbDatabase struct {
|
||||
|
||||
Version tools.MongodbVersion `json:"version" gorm:"type:text;not null"`
|
||||
|
||||
Host string `json:"host" gorm:"type:text;not null"`
|
||||
Port *int `json:"port" gorm:"type:int"`
|
||||
Username string `json:"username" gorm:"type:text;not null"`
|
||||
Password string `json:"password" gorm:"type:text;not null"`
|
||||
Database string `json:"database" gorm:"type:text;not null"`
|
||||
AuthDatabase string `json:"authDatabase" gorm:"type:text;not null;default:'admin'"`
|
||||
IsHttps bool `json:"isHttps" gorm:"type:boolean;default:false"`
|
||||
IsSrv bool `json:"isSrv" gorm:"column:is_srv;type:boolean;not null;default:false"`
|
||||
CpuCount int `json:"cpuCount" gorm:"column:cpu_count;type:int;not null;default:1"`
|
||||
Host string `json:"host" gorm:"type:text;not null"`
|
||||
Port *int `json:"port" gorm:"type:int"`
|
||||
Username string `json:"username" gorm:"type:text;not null"`
|
||||
Password string `json:"password" gorm:"type:text;not null"`
|
||||
Database string `json:"database" gorm:"type:text;not null"`
|
||||
AuthDatabase string `json:"authDatabase" gorm:"type:text;not null;default:'admin'"`
|
||||
IsHttps bool `json:"isHttps" gorm:"type:boolean;default:false"`
|
||||
IsSrv bool `json:"isSrv" gorm:"column:is_srv;type:boolean;not null;default:false"`
|
||||
IsDirectConnection bool `json:"isDirectConnection" gorm:"column:is_direct_connection;type:boolean;not null;default:false"`
|
||||
CpuCount int `json:"cpuCount" gorm:"column:cpu_count;type:int;not null;default:1"`
|
||||
}
|
||||
|
||||
func (m *MongodbDatabase) TableName() string {
|
||||
@@ -132,6 +133,7 @@ func (m *MongodbDatabase) Update(incoming *MongodbDatabase) {
|
||||
m.AuthDatabase = incoming.AuthDatabase
|
||||
m.IsHttps = incoming.IsHttps
|
||||
m.IsSrv = incoming.IsSrv
|
||||
m.IsDirectConnection = incoming.IsDirectConnection
|
||||
m.CpuCount = incoming.CpuCount
|
||||
|
||||
if incoming.Password != "" {
|
||||
@@ -457,9 +459,12 @@ func (m *MongodbDatabase) buildConnectionURI(password string) string {
|
||||
authDB = "admin"
|
||||
}
|
||||
|
||||
tlsParams := ""
|
||||
extraParams := ""
|
||||
if m.IsHttps {
|
||||
tlsParams = "&tls=true&tlsInsecure=true"
|
||||
extraParams += "&tls=true&tlsInsecure=true"
|
||||
}
|
||||
if m.IsDirectConnection {
|
||||
extraParams += "&directConnection=true"
|
||||
}
|
||||
|
||||
if m.IsSrv {
|
||||
@@ -470,7 +475,7 @@ func (m *MongodbDatabase) buildConnectionURI(password string) string {
|
||||
m.Host,
|
||||
m.Database,
|
||||
authDB,
|
||||
tlsParams,
|
||||
extraParams,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -487,7 +492,7 @@ func (m *MongodbDatabase) buildConnectionURI(password string) string {
|
||||
port,
|
||||
m.Database,
|
||||
authDB,
|
||||
tlsParams,
|
||||
extraParams,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -498,9 +503,12 @@ func (m *MongodbDatabase) BuildMongodumpURI(password string) string {
|
||||
authDB = "admin"
|
||||
}
|
||||
|
||||
tlsParams := ""
|
||||
extraParams := ""
|
||||
if m.IsHttps {
|
||||
tlsParams = "&tls=true&tlsInsecure=true"
|
||||
extraParams += "&tls=true&tlsInsecure=true"
|
||||
}
|
||||
if m.IsDirectConnection {
|
||||
extraParams += "&directConnection=true"
|
||||
}
|
||||
|
||||
if m.IsSrv {
|
||||
@@ -510,7 +518,7 @@ func (m *MongodbDatabase) BuildMongodumpURI(password string) string {
|
||||
url.QueryEscape(password),
|
||||
m.Host,
|
||||
authDB,
|
||||
tlsParams,
|
||||
extraParams,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -526,7 +534,7 @@ func (m *MongodbDatabase) BuildMongodumpURI(password string) string {
|
||||
m.Host,
|
||||
port,
|
||||
authDB,
|
||||
tlsParams,
|
||||
extraParams,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -631,6 +631,89 @@ func Test_Validate_SrvConnection_AllowsNullPort(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_BuildConnectionURI_WithDirectConnection_ReturnsCorrectUri(t *testing.T) {
|
||||
port := 27017
|
||||
model := &MongodbDatabase{
|
||||
Host: "mongo.example.local",
|
||||
Port: &port,
|
||||
Username: "testuser",
|
||||
Password: "testpass123",
|
||||
Database: "mydb",
|
||||
AuthDatabase: "admin",
|
||||
IsHttps: false,
|
||||
IsSrv: false,
|
||||
IsDirectConnection: true,
|
||||
}
|
||||
|
||||
uri := model.buildConnectionURI("testpass123")
|
||||
|
||||
assert.Contains(t, uri, "mongodb://")
|
||||
assert.Contains(t, uri, "directConnection=true")
|
||||
assert.Contains(t, uri, "mongo.example.local:27017")
|
||||
assert.Contains(t, uri, "authSource=admin")
|
||||
}
|
||||
|
||||
func Test_BuildConnectionURI_WithoutDirectConnection_OmitsParam(t *testing.T) {
|
||||
port := 27017
|
||||
model := &MongodbDatabase{
|
||||
Host: "localhost",
|
||||
Port: &port,
|
||||
Username: "testuser",
|
||||
Password: "testpass123",
|
||||
Database: "mydb",
|
||||
AuthDatabase: "admin",
|
||||
IsHttps: false,
|
||||
IsSrv: false,
|
||||
IsDirectConnection: false,
|
||||
}
|
||||
|
||||
uri := model.buildConnectionURI("testpass123")
|
||||
|
||||
assert.NotContains(t, uri, "directConnection")
|
||||
}
|
||||
|
||||
func Test_BuildMongodumpURI_WithDirectConnection_ReturnsCorrectUri(t *testing.T) {
|
||||
port := 27017
|
||||
model := &MongodbDatabase{
|
||||
Host: "mongo.example.local",
|
||||
Port: &port,
|
||||
Username: "testuser",
|
||||
Password: "testpass123",
|
||||
Database: "mydb",
|
||||
AuthDatabase: "admin",
|
||||
IsHttps: false,
|
||||
IsSrv: false,
|
||||
IsDirectConnection: true,
|
||||
}
|
||||
|
||||
uri := model.BuildMongodumpURI("testpass123")
|
||||
|
||||
assert.Contains(t, uri, "mongodb://")
|
||||
assert.Contains(t, uri, "directConnection=true")
|
||||
assert.NotContains(t, uri, "/mydb")
|
||||
}
|
||||
|
||||
func Test_BuildConnectionURI_WithDirectConnectionAndTls_ReturnsBothParams(t *testing.T) {
|
||||
port := 27017
|
||||
model := &MongodbDatabase{
|
||||
Host: "mongo.example.local",
|
||||
Port: &port,
|
||||
Username: "testuser",
|
||||
Password: "testpass123",
|
||||
Database: "mydb",
|
||||
AuthDatabase: "admin",
|
||||
IsHttps: true,
|
||||
IsSrv: false,
|
||||
IsDirectConnection: true,
|
||||
}
|
||||
|
||||
uri := model.buildConnectionURI("testpass123")
|
||||
|
||||
assert.Contains(t, uri, "directConnection=true")
|
||||
assert.Contains(t, uri, "tls=true")
|
||||
assert.Contains(t, uri, "tlsInsecure=true")
|
||||
}
|
||||
|
||||
func Test_Validate_StandardConnection_RequiresPort(t *testing.T) {
|
||||
model := &MongodbDatabase{
|
||||
Host: "localhost",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE mongodb_databases ADD COLUMN is_direct_connection BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE mongodb_databases DROP COLUMN is_direct_connection;
|
||||
-- +goose StatementEnd
|
||||
@@ -456,6 +456,46 @@ describe('MongodbConnectionStringParser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Direct Connection Handling', () => {
|
||||
it('should parse directConnection=true from URI', () => {
|
||||
const result = expectSuccess(
|
||||
MongodbConnectionStringParser.parse(
|
||||
'mongodb://user:pass@host:27017/db?authSource=admin&directConnection=true',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.isDirectConnection).toBe(true);
|
||||
});
|
||||
|
||||
it('should default isDirectConnection to false when not specified in URI', () => {
|
||||
const result = expectSuccess(
|
||||
MongodbConnectionStringParser.parse('mongodb://user:pass@host:27017/db'),
|
||||
);
|
||||
|
||||
expect(result.isDirectConnection).toBe(false);
|
||||
});
|
||||
|
||||
it('should parse isDirectConnection=true from key-value format', () => {
|
||||
const result = expectSuccess(
|
||||
MongodbConnectionStringParser.parse(
|
||||
'host=localhost port=27017 database=mydb user=admin password=secret directConnection=true',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.isDirectConnection).toBe(true);
|
||||
});
|
||||
|
||||
it('should default isDirectConnection to false in key-value format when not specified', () => {
|
||||
const result = expectSuccess(
|
||||
MongodbConnectionStringParser.parse(
|
||||
'host=localhost port=27017 database=mydb user=admin password=secret',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.isDirectConnection).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Password Placeholder Handling', () => {
|
||||
it('should treat <db_password> placeholder as empty password in URI format', () => {
|
||||
const result = expectSuccess(
|
||||
|
||||
@@ -7,6 +7,7 @@ export type ParseResult = {
|
||||
authDatabase: string;
|
||||
useTls: boolean;
|
||||
isSrv: boolean;
|
||||
isDirectConnection: boolean;
|
||||
};
|
||||
|
||||
export type ParseError = {
|
||||
@@ -69,6 +70,7 @@ export class MongodbConnectionStringParser {
|
||||
const database = decodeURIComponent(url.pathname.slice(1));
|
||||
const authDatabase = this.getAuthSource(url.search) || 'admin';
|
||||
const useTls = isSrv ? true : this.checkTlsMode(url.search);
|
||||
const isDirectConnection = this.checkDirectConnection(url.search);
|
||||
|
||||
if (!host) {
|
||||
return { error: 'Host is missing from connection string' };
|
||||
@@ -87,6 +89,7 @@ export class MongodbConnectionStringParser {
|
||||
authDatabase,
|
||||
useTls,
|
||||
isSrv,
|
||||
isDirectConnection,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
@@ -133,6 +136,7 @@ export class MongodbConnectionStringParser {
|
||||
}
|
||||
|
||||
const useTls = this.isTlsEnabled(tls);
|
||||
const isDirectConnection = params['directConnection'] === 'true';
|
||||
|
||||
return {
|
||||
host,
|
||||
@@ -143,6 +147,7 @@ export class MongodbConnectionStringParser {
|
||||
authDatabase,
|
||||
useTls,
|
||||
isSrv: false,
|
||||
isDirectConnection,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
@@ -162,6 +167,16 @@ export class MongodbConnectionStringParser {
|
||||
return params.get('authSource') || params.get('authDatabase') || undefined;
|
||||
}
|
||||
|
||||
private static checkDirectConnection(queryString: string | undefined | null): boolean {
|
||||
if (!queryString) return false;
|
||||
|
||||
const params = new URLSearchParams(
|
||||
queryString.startsWith('?') ? queryString.slice(1) : queryString,
|
||||
);
|
||||
|
||||
return params.get('directConnection') === 'true';
|
||||
}
|
||||
|
||||
private static checkTlsMode(queryString: string | undefined | null): boolean {
|
||||
if (!queryString) return false;
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@ export interface MongodbDatabase {
|
||||
authDatabase: string;
|
||||
isHttps: boolean;
|
||||
isSrv: boolean;
|
||||
isDirectConnection: boolean;
|
||||
cpuCount: number;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export const BackupsComponent = ({ database, isCanManageDBs, scrollContainerRef
|
||||
const [showingRestoresBackupId, setShowingRestoresBackupId] = useState<string | undefined>();
|
||||
|
||||
const lastRequestTimeRef = useRef<number>(0);
|
||||
const isBackupsRequestInFlightRef = useRef(false);
|
||||
|
||||
const [downloadingBackupId, setDownloadingBackupId] = useState<string | undefined>();
|
||||
const [cancellingBackupId, setCancellingBackupId] = useState<string | undefined>();
|
||||
@@ -72,6 +73,9 @@ export const BackupsComponent = ({ database, isCanManageDBs, scrollContainerRef
|
||||
};
|
||||
|
||||
const loadBackups = async (limit?: number) => {
|
||||
if (isBackupsRequestInFlightRef.current) return;
|
||||
isBackupsRequestInFlightRef.current = true;
|
||||
|
||||
const requestTime = Date.now();
|
||||
lastRequestTimeRef.current = requestTime;
|
||||
|
||||
@@ -89,6 +93,8 @@ export const BackupsComponent = ({ database, isCanManageDBs, scrollContainerRef
|
||||
if (lastRequestTimeRef.current === requestTime) {
|
||||
alert((e as Error).message);
|
||||
}
|
||||
} finally {
|
||||
isBackupsRequestInFlightRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -253,9 +253,14 @@ export const EditBackupConfigComponent = ({
|
||||
timeOfDay: '00:00',
|
||||
},
|
||||
storage: undefined,
|
||||
retentionPolicyType: RetentionPolicyType.GFS,
|
||||
retentionTimePeriod:
|
||||
plan.maxStoragePeriod === Period.FOREVER ? Period.THREE_MONTH : plan.maxStoragePeriod,
|
||||
retentionPolicyType: IS_CLOUD
|
||||
? RetentionPolicyType.GFS
|
||||
: RetentionPolicyType.TimePeriod,
|
||||
retentionTimePeriod: IS_CLOUD
|
||||
? plan.maxStoragePeriod === Period.FOREVER
|
||||
? Period.THREE_MONTH
|
||||
: plan.maxStoragePeriod
|
||||
: Period.THREE_MONTH,
|
||||
retentionCount: 100,
|
||||
retentionGfsHours: 24,
|
||||
retentionGfsDays: 7,
|
||||
|
||||
@@ -46,7 +46,10 @@ export const EditMongoDbSpecificDataComponent = ({
|
||||
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
||||
const [isConnectionFailed, setIsConnectionFailed] = useState(false);
|
||||
|
||||
const hasAdvancedValues = !!database.mongodb?.authDatabase || !!database.mongodb?.isSrv;
|
||||
const hasAdvancedValues =
|
||||
!!database.mongodb?.authDatabase ||
|
||||
!!database.mongodb?.isSrv ||
|
||||
!!database.mongodb?.isDirectConnection;
|
||||
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||
|
||||
const parseFromClipboard = async () => {
|
||||
@@ -80,11 +83,12 @@ export const EditMongoDbSpecificDataComponent = ({
|
||||
authDatabase: result.authDatabase,
|
||||
isHttps: result.useTls,
|
||||
isSrv: result.isSrv,
|
||||
isDirectConnection: result.isDirectConnection,
|
||||
cpuCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
if (result.isSrv) {
|
||||
if (result.isSrv || result.isDirectConnection) {
|
||||
setShowAdvanced(true);
|
||||
}
|
||||
|
||||
@@ -407,6 +411,31 @@ export const EditMongoDbSpecificDataComponent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[150px]">Direct connection</div>
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
checked={editingDatabase.mongodb?.isDirectConnection || false}
|
||||
onChange={(checked) => {
|
||||
if (!editingDatabase.mongodb) return;
|
||||
|
||||
setEditingDatabase({
|
||||
...editingDatabase,
|
||||
mongodb: { ...editingDatabase.mongodb, isDirectConnection: checked },
|
||||
});
|
||||
setIsConnectionTested(false);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Connect directly to a single server, skipping replica set discovery. Useful when the server is behind a load balancer, proxy or tunnel."
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[150px]">Auth database</div>
|
||||
<Input
|
||||
|
||||
@@ -42,6 +42,13 @@ export const ShowMongoDbSpecificDataComponent = ({ database }: Props) => {
|
||||
<div>{database.mongodb?.cpuCount}</div>
|
||||
</div>
|
||||
|
||||
{database.mongodb?.isDirectConnection && (
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[150px]">Direct connection</div>
|
||||
<div>Yes</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{database.mongodb?.authDatabase && (
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="min-w-[150px]">Auth database</div>
|
||||
|
||||
Reference in New Issue
Block a user