@agenticmail/enterprise 0.5.285 → 0.5.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-heartbeat-NFFDMZFV.js +510 -0
- package/dist/chunk-3LCTQEDL.js +1224 -0
- package/dist/chunk-CB6QYN3G.js +1272 -0
- package/dist/chunk-NHLKS3CD.js +4739 -0
- package/dist/chunk-PABLWR7B.js +3780 -0
- package/dist/cli-agent-XTKC4A34.js +1778 -0
- package/dist/cli-serve-S5CDI7ZX.js +114 -0
- package/dist/cli.js +3 -3
- package/dist/connection-manager-6VUG3MFS.js +7 -0
- package/dist/index.js +3 -3
- package/dist/routes-IJRJL2SF.js +13695 -0
- package/dist/runtime-RVVOICEF.js +45 -0
- package/dist/server-CVBX4DPS.js +15 -0
- package/dist/setup-FSEN2QRL.js +20 -0
- package/package.json +1 -1
- package/src/database-access/connection-manager.ts +357 -138
|
@@ -722,72 +722,169 @@ export class DatabaseConnectionManager {
|
|
|
722
722
|
this.drivers.set(type, driver);
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
+
// ─── Auto-Install Helper ──────────────────────────────────────────────────
|
|
726
|
+
|
|
727
|
+
private _installCache = new Set<string>();
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Auto-install a npm package if not already installed.
|
|
731
|
+
* Caches install attempts to avoid repeated installs in the same process.
|
|
732
|
+
*/
|
|
733
|
+
private async ensurePackage(pkg: string): Promise<any> {
|
|
734
|
+
try {
|
|
735
|
+
return await import(pkg);
|
|
736
|
+
} catch {
|
|
737
|
+
// Package not installed — auto-install it
|
|
738
|
+
if (this._installCache.has(pkg)) {
|
|
739
|
+
throw new Error(`Package "${pkg}" could not be loaded after auto-install. Please install manually: npm install ${pkg}`);
|
|
740
|
+
}
|
|
741
|
+
this._installCache.add(pkg);
|
|
742
|
+
console.log(`[database-access] Auto-installing "${pkg}"...`);
|
|
743
|
+
const { execSync } = await import('child_process');
|
|
744
|
+
try {
|
|
745
|
+
execSync(`npm install --no-save ${pkg}`, {
|
|
746
|
+
stdio: 'pipe',
|
|
747
|
+
timeout: 120_000,
|
|
748
|
+
cwd: process.cwd(),
|
|
749
|
+
});
|
|
750
|
+
console.log(`[database-access] Successfully installed "${pkg}"`);
|
|
751
|
+
// Clear Node's module cache to pick up the new package
|
|
752
|
+
return await import(pkg);
|
|
753
|
+
} catch (installErr: any) {
|
|
754
|
+
const msg = installErr.stderr?.toString?.().slice(0, 200) || installErr.message;
|
|
755
|
+
throw new Error(`Failed to auto-install "${pkg}": ${msg}. Please install manually: npm install ${pkg}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Connect with a timeout wrapper — all drivers get a 15s connect timeout.
|
|
762
|
+
*/
|
|
763
|
+
private async connectWithTimeout<T>(fn: () => Promise<T>, timeoutMs = 15_000, label = 'Connection'): Promise<T> {
|
|
764
|
+
return new Promise<T>((resolve, reject) => {
|
|
765
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms — check your host/port and ensure the database is reachable`)), timeoutMs);
|
|
766
|
+
fn().then(
|
|
767
|
+
(v) => { clearTimeout(timer); resolve(v); },
|
|
768
|
+
(e) => { clearTimeout(timer); reject(e); },
|
|
769
|
+
);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Detect SSL requirement from connection string or cloud provider type.
|
|
775
|
+
* Cloud-hosted databases almost always require SSL.
|
|
776
|
+
*/
|
|
777
|
+
private needsSsl(config: any, credentials?: any): boolean | Record<string, any> {
|
|
778
|
+
if (config.ssl === true) return true;
|
|
779
|
+
if (config.ssl === false) return false;
|
|
780
|
+
// Auto-enable SSL for cloud providers
|
|
781
|
+
const cloudTypes: string[] = ['supabase', 'neon', 'planetscale', 'cockroachdb', 'turso', 'upstash'];
|
|
782
|
+
if (cloudTypes.includes(config.type)) return true;
|
|
783
|
+
// Detect from connection string
|
|
784
|
+
const connStr = credentials?.connectionString || '';
|
|
785
|
+
if (connStr.includes('sslmode=require') || connStr.includes('ssl=true')) return true;
|
|
786
|
+
// Supabase/Neon/etc URLs contain their domain
|
|
787
|
+
if (/supabase|neon\.tech|cockroachlabs|planetscale|turso\.io|railway\.app|render\.com|aiven\.io|timescale\.com/i.test(connStr)) return true;
|
|
788
|
+
if (/supabase|neon\.tech|cockroachlabs|planetscale|turso\.io|railway|render|aiven|timescale/i.test(config.host || '')) return true;
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Parse a connection string to extract host/port/database/username when fields are missing.
|
|
794
|
+
*/
|
|
795
|
+
private parseConnectionString(connStr: string, type: string): { host?: string; port?: number; database?: string; username?: string } {
|
|
796
|
+
try {
|
|
797
|
+
// Handle postgres://, mysql://, mongodb://, redis://, libsql:// etc
|
|
798
|
+
const cleaned = connStr.replace(/^(postgres|postgresql|mysql|mongodb\+srv|mongodb|redis|rediss|libsql)/, 'http');
|
|
799
|
+
const url = new URL(cleaned);
|
|
800
|
+
return {
|
|
801
|
+
host: url.hostname || undefined,
|
|
802
|
+
port: url.port ? parseInt(url.port) : undefined,
|
|
803
|
+
database: url.pathname?.replace(/^\//, '') || undefined,
|
|
804
|
+
username: url.username || undefined,
|
|
805
|
+
};
|
|
806
|
+
} catch {
|
|
807
|
+
return {};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
725
811
|
private registerBuiltinDrivers(): void {
|
|
726
|
-
|
|
812
|
+
const self = this;
|
|
813
|
+
|
|
814
|
+
// ── PostgreSQL / CockroachDB / Supabase / Neon ─────────────────────────
|
|
727
815
|
const pgDriver: DatabaseDriver = {
|
|
728
816
|
async connect(config, credentials) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
817
|
+
const pgMod = await self.ensurePackage('postgres');
|
|
818
|
+
const pgFn = pgMod.default || pgMod;
|
|
819
|
+
|
|
820
|
+
// Build connection string from parts if not provided
|
|
821
|
+
let connStr = credentials.connectionString;
|
|
822
|
+
if (!connStr) {
|
|
823
|
+
const user = encodeURIComponent(config.username || 'postgres');
|
|
824
|
+
const pass = encodeURIComponent(credentials.password || '');
|
|
825
|
+
const host = config.host || 'localhost';
|
|
826
|
+
const port = config.port || 5432;
|
|
827
|
+
const db = config.database || 'postgres';
|
|
828
|
+
connStr = `postgresql://${user}:${pass}@${host}:${port}/${db}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const ssl = self.needsSsl(config, credentials);
|
|
832
|
+
const sql: any = await self.connectWithTimeout(() => {
|
|
833
|
+
const s = pgFn(connStr, {
|
|
733
834
|
max: config.pool?.max ?? 10,
|
|
734
|
-
idle_timeout: (config.pool?.idleTimeoutMs ??
|
|
735
|
-
connect_timeout:
|
|
736
|
-
ssl:
|
|
835
|
+
idle_timeout: (config.pool?.idleTimeoutMs ?? 30_000) / 1000,
|
|
836
|
+
connect_timeout: 10,
|
|
837
|
+
ssl: ssl ? (config.sslRejectUnauthorized === false ? 'prefer' as any : 'require' as any) : false,
|
|
737
838
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
: await sql.unsafe(q);
|
|
743
|
-
return {
|
|
744
|
-
rows: [...result],
|
|
745
|
-
affectedRows: result.count,
|
|
746
|
-
fields: result.columns?.map((c: any) => ({ name: c.name, type: String(c.type) })),
|
|
747
|
-
};
|
|
748
|
-
},
|
|
749
|
-
async close() { await sql.end(); },
|
|
750
|
-
async ping() { try { await sql`SELECT 1`; return true; } catch { return false; } },
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
// Fallback: construct connection from parts
|
|
754
|
-
const connStr = `postgresql://${encodeURIComponent(config.username || '')}:${encodeURIComponent(credentials.password || '')}@${config.host || 'localhost'}:${config.port || 5432}/${config.database || 'postgres'}`;
|
|
755
|
-
const pgMod2 = await import('postgres' as string);
|
|
756
|
-
const postgres = pgMod2.default || pgMod2;
|
|
757
|
-
const sql = postgres(connStr, {
|
|
758
|
-
max: config.pool?.max ?? 10,
|
|
759
|
-
ssl: config.ssl ? {} : false,
|
|
760
|
-
});
|
|
839
|
+
// Force a connection attempt so errors surface now
|
|
840
|
+
return s`SELECT 1`.then(() => s);
|
|
841
|
+
}, 15_000, 'PostgreSQL');
|
|
842
|
+
|
|
761
843
|
return {
|
|
762
844
|
async query(q: string, params?: any[]) {
|
|
763
845
|
const result = params?.length ? await sql.unsafe(q, params) : await sql.unsafe(q);
|
|
764
|
-
return {
|
|
846
|
+
return {
|
|
847
|
+
rows: [...result],
|
|
848
|
+
affectedRows: result.count,
|
|
849
|
+
fields: result.columns?.map((c: any) => ({ name: c.name, type: String(c.type) })),
|
|
850
|
+
};
|
|
765
851
|
},
|
|
766
|
-
async close() { await sql.end(); },
|
|
852
|
+
async close() { try { await sql.end({ timeout: 5 }); } catch {} },
|
|
767
853
|
async ping() { try { await sql`SELECT 1`; return true; } catch { return false; } },
|
|
768
854
|
};
|
|
769
855
|
},
|
|
770
856
|
};
|
|
771
|
-
|
|
772
857
|
this.drivers.set('postgresql', pgDriver);
|
|
773
858
|
this.drivers.set('cockroachdb', pgDriver);
|
|
774
859
|
this.drivers.set('supabase', pgDriver);
|
|
775
860
|
this.drivers.set('neon', pgDriver);
|
|
776
861
|
|
|
777
|
-
// MySQL / MariaDB / PlanetScale
|
|
862
|
+
// ── MySQL / MariaDB / PlanetScale ──────────────────────────────────────
|
|
778
863
|
const mysqlDriver: DatabaseDriver = {
|
|
779
864
|
async connect(config, credentials) {
|
|
780
|
-
const mysql2 = await
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
865
|
+
const mysql2 = await self.ensurePackage('mysql2/promise');
|
|
866
|
+
|
|
867
|
+
// Parse connection string for missing fields
|
|
868
|
+
const parsed = credentials.connectionString ? self.parseConnectionString(credentials.connectionString, config.type) : {};
|
|
869
|
+
const ssl = self.needsSsl(config, credentials);
|
|
870
|
+
|
|
871
|
+
const pool = await self.connectWithTimeout(async () => {
|
|
872
|
+
const p = mysql2.createPool({
|
|
873
|
+
host: config.host || parsed.host || 'localhost',
|
|
874
|
+
port: config.port || parsed.port || 3306,
|
|
875
|
+
user: config.username || parsed.username,
|
|
876
|
+
password: credentials.password,
|
|
877
|
+
database: config.database || parsed.database,
|
|
878
|
+
connectionLimit: config.pool?.max ?? 10,
|
|
879
|
+
connectTimeout: 10_000,
|
|
880
|
+
ssl: ssl ? { rejectUnauthorized: config.sslRejectUnauthorized !== false } : undefined,
|
|
881
|
+
uri: credentials.connectionString || undefined,
|
|
882
|
+
});
|
|
883
|
+
// Validate connection
|
|
884
|
+
await p.execute('SELECT 1');
|
|
885
|
+
return p;
|
|
886
|
+
}, 15_000, 'MySQL');
|
|
887
|
+
|
|
791
888
|
return {
|
|
792
889
|
async query(q: string, params?: any[]) {
|
|
793
890
|
const [rows, fields] = await pool.execute(q, params);
|
|
@@ -798,51 +895,55 @@ export class DatabaseConnectionManager {
|
|
|
798
895
|
fields: (fields as any[])?.map((f: any) => ({ name: f.name, type: String(f.type) })),
|
|
799
896
|
};
|
|
800
897
|
},
|
|
801
|
-
async close() { await pool.end(); },
|
|
898
|
+
async close() { try { await pool.end(); } catch {} },
|
|
802
899
|
async ping() { try { await pool.execute('SELECT 1'); return true; } catch { return false; } },
|
|
803
900
|
};
|
|
804
901
|
},
|
|
805
902
|
};
|
|
806
|
-
|
|
807
903
|
this.drivers.set('mysql', mysqlDriver);
|
|
808
904
|
this.drivers.set('mariadb', mysqlDriver);
|
|
809
905
|
this.drivers.set('planetscale', mysqlDriver);
|
|
810
906
|
|
|
811
|
-
// SQLite
|
|
907
|
+
// ── SQLite ─────────────────────────────────────────────────────────────
|
|
812
908
|
this.drivers.set('sqlite', {
|
|
813
|
-
async connect(config,
|
|
814
|
-
const
|
|
815
|
-
const
|
|
909
|
+
async connect(config, credentials) {
|
|
910
|
+
const sqliteMod = await self.ensurePackage('better-sqlite3');
|
|
911
|
+
const Database = sqliteMod.default || sqliteMod;
|
|
912
|
+
const dbPath = credentials.connectionString || config.database || ':memory:';
|
|
913
|
+
const db = new Database(dbPath, { readonly: false });
|
|
914
|
+
// Enable WAL mode for better concurrency
|
|
915
|
+
try { db.pragma('journal_mode = WAL'); } catch {}
|
|
816
916
|
return {
|
|
817
917
|
async query(q: string, params?: any[]) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
} catch (err: any) {
|
|
829
|
-
throw err;
|
|
918
|
+
const trimmed = q.trim().toUpperCase();
|
|
919
|
+
if (trimmed.startsWith('SELECT') || trimmed.startsWith('WITH') || trimmed.startsWith('PRAGMA') || trimmed.startsWith('EXPLAIN')) {
|
|
920
|
+
const stmt = db.prepare(q);
|
|
921
|
+
const rows = params?.length ? stmt.all(...params) : stmt.all();
|
|
922
|
+
return { rows, fields: rows.length > 0 ? Object.keys(rows[0]).map(k => ({ name: k, type: 'unknown' })) : [] };
|
|
923
|
+
} else {
|
|
924
|
+
const stmt = db.prepare(q);
|
|
925
|
+
const result = params?.length ? stmt.run(...params) : stmt.run();
|
|
926
|
+
return { rows: [], affectedRows: result.changes };
|
|
830
927
|
}
|
|
831
928
|
},
|
|
832
|
-
async close() { db.close(); },
|
|
929
|
+
async close() { try { db.close(); } catch {} },
|
|
833
930
|
async ping() { try { db.prepare('SELECT 1').get(); return true; } catch { return false; } },
|
|
834
931
|
};
|
|
835
932
|
},
|
|
836
933
|
});
|
|
837
934
|
|
|
838
|
-
// Turso / LibSQL
|
|
935
|
+
// ── Turso / LibSQL ─────────────────────────────────────────────────────
|
|
839
936
|
this.drivers.set('turso', {
|
|
840
937
|
async connect(config, credentials) {
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
});
|
|
938
|
+
const libsql = await self.ensurePackage('@libsql/client');
|
|
939
|
+
const { createClient } = libsql;
|
|
940
|
+
|
|
941
|
+
const url = credentials.connectionString || `libsql://${config.host}`;
|
|
942
|
+
const client = createClient({ url, authToken: credentials.password });
|
|
943
|
+
|
|
944
|
+
// Validate
|
|
945
|
+
await self.connectWithTimeout(() => client.execute('SELECT 1'), 15_000, 'Turso');
|
|
946
|
+
|
|
846
947
|
return {
|
|
847
948
|
async query(q: string, params?: any[]) {
|
|
848
949
|
const result = await client.execute({ sql: q, args: params || [] });
|
|
@@ -852,91 +953,159 @@ export class DatabaseConnectionManager {
|
|
|
852
953
|
fields: result.columns?.map((c: any) => ({ name: String(c), type: 'unknown' })),
|
|
853
954
|
};
|
|
854
955
|
},
|
|
855
|
-
async close() { client.close(); },
|
|
956
|
+
async close() { try { client.close(); } catch {} },
|
|
856
957
|
async ping() { try { await client.execute('SELECT 1'); return true; } catch { return false; } },
|
|
857
958
|
};
|
|
858
959
|
},
|
|
859
960
|
});
|
|
860
961
|
|
|
861
|
-
// MongoDB
|
|
962
|
+
// ── MongoDB ────────────────────────────────────────────────────────────
|
|
862
963
|
this.drivers.set('mongodb', {
|
|
863
964
|
async connect(config, credentials) {
|
|
864
|
-
const
|
|
865
|
-
const
|
|
965
|
+
const mongoMod = await self.ensurePackage('mongodb');
|
|
966
|
+
const { MongoClient } = mongoMod;
|
|
967
|
+
|
|
968
|
+
const uri = credentials.connectionString || (() => {
|
|
969
|
+
const user = encodeURIComponent(config.username || '');
|
|
970
|
+
const pass = encodeURIComponent(credentials.password || '');
|
|
971
|
+
const host = config.host || 'localhost';
|
|
972
|
+
const port = config.port || 27017;
|
|
973
|
+
const db = config.database || 'admin';
|
|
974
|
+
const auth = user ? `${user}:${pass}@` : '';
|
|
975
|
+
return `mongodb://${auth}${host}:${port}/${db}`;
|
|
976
|
+
})();
|
|
977
|
+
|
|
978
|
+
const ssl = self.needsSsl(config, credentials);
|
|
979
|
+
// mongodb+srv:// always uses TLS
|
|
980
|
+
const useTls = ssl || uri.startsWith('mongodb+srv://');
|
|
981
|
+
|
|
866
982
|
const client = new MongoClient(uri, {
|
|
867
983
|
maxPoolSize: config.pool?.max ?? 10,
|
|
868
|
-
|
|
984
|
+
serverSelectionTimeoutMS: 10_000,
|
|
985
|
+
connectTimeoutMS: 10_000,
|
|
986
|
+
tls: useTls || undefined,
|
|
869
987
|
});
|
|
870
|
-
|
|
871
|
-
|
|
988
|
+
|
|
989
|
+
await self.connectWithTimeout(() => client.connect(), 15_000, 'MongoDB');
|
|
990
|
+
const db = client.db(config.database || self.parseConnectionString(uri, 'mongodb').database);
|
|
991
|
+
|
|
872
992
|
return {
|
|
873
993
|
async query(q: string, _params?: any[]) {
|
|
874
|
-
// MongoDB queries come as JSON strings: { collection: "users", operation: "find", filter: {...} }
|
|
875
994
|
try {
|
|
876
995
|
const cmd = JSON.parse(q);
|
|
877
996
|
const collection = db.collection(cmd.collection);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
997
|
+
switch (cmd.operation) {
|
|
998
|
+
case 'find': {
|
|
999
|
+
const cursor = collection.find(cmd.filter || {});
|
|
1000
|
+
if (cmd.projection) cursor.project(cmd.projection);
|
|
1001
|
+
if (cmd.sort) cursor.sort(cmd.sort);
|
|
1002
|
+
if (cmd.skip) cursor.skip(cmd.skip);
|
|
1003
|
+
cursor.limit(cmd.limit || 100);
|
|
1004
|
+
return { rows: await cursor.toArray() };
|
|
1005
|
+
}
|
|
1006
|
+
case 'findOne': {
|
|
1007
|
+
const doc = await collection.findOne(cmd.filter || {}, { projection: cmd.projection });
|
|
1008
|
+
return { rows: doc ? [doc] : [] };
|
|
1009
|
+
}
|
|
1010
|
+
case 'insertOne': {
|
|
1011
|
+
const r = await collection.insertOne(cmd.document);
|
|
1012
|
+
return { rows: [{ insertedId: r.insertedId }], affectedRows: 1 };
|
|
1013
|
+
}
|
|
1014
|
+
case 'insertMany': {
|
|
1015
|
+
const r = await collection.insertMany(cmd.documents);
|
|
1016
|
+
return { rows: [{ insertedCount: r.insertedCount }], affectedRows: r.insertedCount };
|
|
1017
|
+
}
|
|
1018
|
+
case 'updateOne': case 'updateMany': {
|
|
1019
|
+
const fn = cmd.operation === 'updateOne' ? collection.updateOne.bind(collection) : collection.updateMany.bind(collection);
|
|
1020
|
+
const r = await fn(cmd.filter || {}, cmd.update, { upsert: cmd.upsert });
|
|
1021
|
+
return { rows: [{ matchedCount: r.matchedCount, modifiedCount: r.modifiedCount, upsertedId: r.upsertedId }], affectedRows: r.modifiedCount };
|
|
1022
|
+
}
|
|
1023
|
+
case 'deleteOne': case 'deleteMany': {
|
|
1024
|
+
const fn = cmd.operation === 'deleteOne' ? collection.deleteOne.bind(collection) : collection.deleteMany.bind(collection);
|
|
1025
|
+
const r = await fn(cmd.filter || {});
|
|
1026
|
+
return { rows: [], affectedRows: r.deletedCount };
|
|
1027
|
+
}
|
|
1028
|
+
case 'aggregate': return { rows: await collection.aggregate(cmd.pipeline || []).toArray() };
|
|
1029
|
+
case 'count': case 'countDocuments': return { rows: [{ count: await collection.countDocuments(cmd.filter || {}) }] };
|
|
1030
|
+
case 'distinct': return { rows: [{ values: await collection.distinct(cmd.field, cmd.filter || {}) }] };
|
|
1031
|
+
case 'listCollections': {
|
|
1032
|
+
const cols = await db.listCollections().toArray();
|
|
1033
|
+
return { rows: cols.map((c: any) => ({ name: c.name, type: c.type })) };
|
|
1034
|
+
}
|
|
1035
|
+
default: throw new Error(`Unknown operation: ${cmd.operation}. Supported: find, findOne, insertOne, insertMany, updateOne, updateMany, deleteOne, deleteMany, aggregate, count, distinct, listCollections`);
|
|
904
1036
|
}
|
|
905
|
-
throw new Error(`Unknown MongoDB operation: ${cmd.operation}`);
|
|
906
1037
|
} catch (err: any) {
|
|
907
|
-
if (err.message.startsWith('Unknown MongoDB')) throw err;
|
|
908
1038
|
throw new Error(`MongoDB query error: ${err.message}`);
|
|
909
1039
|
}
|
|
910
1040
|
},
|
|
911
|
-
async close() { await client.close(); },
|
|
1041
|
+
async close() { try { await client.close(); } catch {} },
|
|
912
1042
|
async ping() { try { await db.command({ ping: 1 }); return true; } catch { return false; } },
|
|
913
1043
|
};
|
|
914
1044
|
},
|
|
915
1045
|
});
|
|
916
1046
|
|
|
917
|
-
// Redis
|
|
1047
|
+
// ── Redis (self-hosted or cloud with standard Redis protocol) ──────────
|
|
918
1048
|
this.drivers.set('redis', {
|
|
919
1049
|
async connect(config, credentials) {
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1050
|
+
const host = config.host || 'localhost';
|
|
1051
|
+
const port = config.port || 6379;
|
|
1052
|
+
const ssl = self.needsSsl(config, credentials);
|
|
1053
|
+
const connStr = credentials.connectionString || '';
|
|
1054
|
+
|
|
1055
|
+
// Detect if connection string uses rediss:// (TLS)
|
|
1056
|
+
const useTls = ssl || connStr.startsWith('rediss://');
|
|
1057
|
+
|
|
1058
|
+
// Parse auth from connection string if present
|
|
1059
|
+
let password = credentials.password;
|
|
1060
|
+
let username = config.username;
|
|
1061
|
+
if (connStr) {
|
|
1062
|
+
try {
|
|
1063
|
+
const parsed = self.parseConnectionString(connStr, 'redis');
|
|
1064
|
+
if (!password) {
|
|
1065
|
+
// redis://user:pass@host:port or redis://:pass@host:port
|
|
1066
|
+
const url = new URL(connStr.replace(/^redis(s?)/, 'http'));
|
|
1067
|
+
password = url.password || password;
|
|
1068
|
+
username = url.username || username;
|
|
1069
|
+
}
|
|
1070
|
+
} catch {}
|
|
931
1071
|
}
|
|
932
1072
|
|
|
933
|
-
|
|
1073
|
+
// Use TLS or plain net socket
|
|
1074
|
+
let socket: any;
|
|
1075
|
+
const connectHost = connStr ? (self.parseConnectionString(connStr, 'redis').host || host) : host;
|
|
1076
|
+
const connectPort = connStr ? (self.parseConnectionString(connStr, 'redis').port || port) : port;
|
|
1077
|
+
|
|
1078
|
+
await self.connectWithTimeout(async () => {
|
|
1079
|
+
if (useTls) {
|
|
1080
|
+
const tls = await import('tls');
|
|
1081
|
+
socket = tls.connect({ host: connectHost, port: connectPort, rejectUnauthorized: config.sslRejectUnauthorized !== false });
|
|
1082
|
+
await new Promise<void>((resolve, reject) => {
|
|
1083
|
+
socket.on('secureConnect', resolve);
|
|
1084
|
+
socket.on('error', reject);
|
|
1085
|
+
});
|
|
1086
|
+
} else {
|
|
1087
|
+
const net = await import('net');
|
|
1088
|
+
socket = new net.Socket();
|
|
1089
|
+
await new Promise<void>((resolve, reject) => {
|
|
1090
|
+
socket.connect(connectPort, connectHost, () => resolve());
|
|
1091
|
+
socket.on('error', reject);
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// AUTH if needed — support Redis 6+ ACL: AUTH username password
|
|
1096
|
+
if (password) {
|
|
1097
|
+
const authCmd = username && username !== 'default' ? `AUTH ${username} ${password}` : `AUTH ${password}`;
|
|
1098
|
+
await sendRedisCommand(socket, authCmd);
|
|
1099
|
+
}
|
|
1100
|
+
}, 15_000, 'Redis');
|
|
1101
|
+
|
|
1102
|
+
function sendRedisCommand(sock: any, cmd: string): Promise<string> {
|
|
934
1103
|
return new Promise((resolve, reject) => {
|
|
935
1104
|
let data = '';
|
|
936
|
-
const onData = (chunk: Buffer) => { data += chunk.toString(); sock.off('data', onData); resolve(data); };
|
|
1105
|
+
const onData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('\r\n')) { sock.off('data', onData); resolve(data); } };
|
|
937
1106
|
sock.on('data', onData);
|
|
938
1107
|
sock.write(cmd + '\r\n');
|
|
939
|
-
setTimeout(() => { sock.off('data', onData); reject(new Error('Redis
|
|
1108
|
+
setTimeout(() => { sock.off('data', onData); reject(new Error('Redis command timed out after 10s')); }, 10_000);
|
|
940
1109
|
});
|
|
941
1110
|
}
|
|
942
1111
|
|
|
@@ -945,36 +1114,86 @@ export class DatabaseConnectionManager {
|
|
|
945
1114
|
const result = await sendRedisCommand(socket, q);
|
|
946
1115
|
return { rows: [{ result: result.trim() }] };
|
|
947
1116
|
},
|
|
948
|
-
async close() { socket.destroy(); },
|
|
1117
|
+
async close() { try { socket.destroy(); } catch {} },
|
|
949
1118
|
async ping() { try { const r = await sendRedisCommand(socket, 'PING'); return r.includes('PONG'); } catch { return false; } },
|
|
950
1119
|
};
|
|
951
1120
|
},
|
|
952
1121
|
});
|
|
953
1122
|
|
|
954
|
-
// Upstash Redis
|
|
955
|
-
this.
|
|
1123
|
+
// ── Upstash Redis (REST API — zero dependencies) ──────────────────────
|
|
1124
|
+
this.drivers.set('upstash', {
|
|
956
1125
|
async connect(config, credentials) {
|
|
957
|
-
// Upstash
|
|
958
|
-
|
|
959
|
-
|
|
1126
|
+
// Upstash REST: https://<endpoint>.upstash.io with Bearer token
|
|
1127
|
+
// Connection string format: https://<token>@<endpoint>.upstash.io or just the URL
|
|
1128
|
+
let baseUrl = '';
|
|
1129
|
+
let token = credentials.password || '';
|
|
1130
|
+
|
|
1131
|
+
if (credentials.connectionString) {
|
|
1132
|
+
const cs = credentials.connectionString;
|
|
1133
|
+
// Handle https://token@host format
|
|
1134
|
+
if (cs.startsWith('https://') && cs.includes('@')) {
|
|
1135
|
+
try {
|
|
1136
|
+
const url = new URL(cs);
|
|
1137
|
+
token = token || url.username || url.password;
|
|
1138
|
+
baseUrl = `https://${url.host}`;
|
|
1139
|
+
} catch {
|
|
1140
|
+
baseUrl = cs;
|
|
1141
|
+
}
|
|
1142
|
+
} else if (cs.startsWith('https://')) {
|
|
1143
|
+
baseUrl = cs.replace(/\/$/, '');
|
|
1144
|
+
} else if (cs.startsWith('rediss://')) {
|
|
1145
|
+
// Upstash also provides redis:// URLs — extract host for REST
|
|
1146
|
+
try {
|
|
1147
|
+
const url = new URL(cs.replace('rediss://', 'https://'));
|
|
1148
|
+
token = token || url.password;
|
|
1149
|
+
baseUrl = `https://${url.hostname}`;
|
|
1150
|
+
} catch {
|
|
1151
|
+
baseUrl = `https://${config.host}`;
|
|
1152
|
+
}
|
|
1153
|
+
} else {
|
|
1154
|
+
baseUrl = `https://${cs}`;
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
baseUrl = `https://${config.host}`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (!baseUrl) throw new Error('Upstash requires a REST URL or hostname. Find it in your Upstash console under REST API.');
|
|
1161
|
+
if (!token) throw new Error('Upstash requires an auth token. Find it in your Upstash console under REST API.');
|
|
960
1162
|
|
|
961
1163
|
async function upstashRequest(command: string[]): Promise<any> {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1164
|
+
const ctrl = new AbortController();
|
|
1165
|
+
const timer = setTimeout(() => ctrl.abort(), 10_000);
|
|
1166
|
+
try {
|
|
1167
|
+
const resp = await fetch(baseUrl, {
|
|
1168
|
+
method: 'POST',
|
|
1169
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
1170
|
+
body: JSON.stringify(command),
|
|
1171
|
+
signal: ctrl.signal,
|
|
1172
|
+
});
|
|
1173
|
+
if (!resp.ok) {
|
|
1174
|
+
const body = await resp.text().catch(() => '');
|
|
1175
|
+
throw new Error(`Upstash HTTP ${resp.status}: ${body.slice(0, 200)}`);
|
|
1176
|
+
}
|
|
1177
|
+
return resp.json();
|
|
1178
|
+
} finally {
|
|
1179
|
+
clearTimeout(timer);
|
|
1180
|
+
}
|
|
969
1181
|
}
|
|
970
1182
|
|
|
1183
|
+
// Validate connection
|
|
1184
|
+
await self.connectWithTimeout(async () => {
|
|
1185
|
+
const r = await upstashRequest(['PING']);
|
|
1186
|
+
if (r.result !== 'PONG') throw new Error('Upstash PING failed — check your token and endpoint URL');
|
|
1187
|
+
}, 15_000, 'Upstash');
|
|
1188
|
+
|
|
971
1189
|
return {
|
|
972
1190
|
async query(q: string) {
|
|
973
1191
|
const parts = q.trim().split(/\s+/);
|
|
974
1192
|
const result = await upstashRequest(parts);
|
|
975
|
-
|
|
1193
|
+
const val = result.result ?? result;
|
|
1194
|
+
return { rows: [{ result: typeof val === 'string' ? val : JSON.stringify(val) }] };
|
|
976
1195
|
},
|
|
977
|
-
async close() { /* stateless REST */ },
|
|
1196
|
+
async close() { /* stateless REST — nothing to close */ },
|
|
978
1197
|
async ping() { try { const r = await upstashRequest(['PING']); return r.result === 'PONG'; } catch { return false; } },
|
|
979
1198
|
};
|
|
980
1199
|
},
|