@mcpher/gas-fakes 2.3.3 → 2.3.5
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/README.md +4 -0
- package/appsscript.json +1 -0
- package/package.json +4 -1
- package/skills-lock.json +10 -0
- package/src/cli/app.js +18 -0
- package/src/cli/setup.js +46 -2
- package/src/index.js +2 -0
- package/src/services/jdbc/app.js +9 -0
- package/src/services/jdbc/fakejdbcconnection.js +45 -0
- package/src/services/jdbc/fakejdbcresultset.js +70 -0
- package/src/services/jdbc/fakejdbcresultsetmetadata.js +39 -0
- package/src/services/jdbc/fakejdbcservice.js +245 -0
- package/src/services/jdbc/fakejdbcstatement.js +39 -0
- package/src/services/spreadsheetapp/fakeembeddedchart.js +80 -0
- package/src/support/sxauth.js +26 -0
- package/src/support/sxjdbc.js +166 -0
- package/src/support/syncit.js +19 -1
- package/src/support/workersync/sxfunctions.js +1 -0
package/README.md
CHANGED
|
@@ -184,6 +184,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
184
184
|
- [getting started](GETTING_STARTED.md) - how to handle authentication for Workspace scopes.
|
|
185
185
|
- [readme](README.md)
|
|
186
186
|
- [gas fakes cli](gas-fakes-cli.md)
|
|
187
|
+
- [github actions using adc](https://github.com/brucemcpherson/gas-fakes-actions-adc)
|
|
188
|
+
- [github actions using dwd and wif](https://github.com/brucemcpherson/gas-fakes-actions-dwd)
|
|
187
189
|
- [ksuite as a back end](ksuite_poc.md)
|
|
188
190
|
- [msgraph as a back end](msgraph.md)
|
|
189
191
|
- [gas-fakes in serverless containers](https://docs.google.com/presentation/d/1JlXF9T--DD4ERHopyP3WyAMhjRCxxHblgCP5ynxaJ3k/edit?usp=sharing)
|
|
@@ -193,6 +195,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
193
195
|
- [running gas-fakes on google kubernetes engine](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
194
196
|
- [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
195
197
|
- [running gas-fakes on Azure ACA](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
198
|
+
- [running gas-fakes on Github actions](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
199
|
+
- [jdbc notes](jdbc-notes.md)
|
|
196
200
|
- [Yes – you can run native apps script code on Azure ACA as well!](https://ramblings.mcpher.com/yes-you-can-run-native-apps-script-code-on-azure-aca-as-well/)
|
|
197
201
|
- [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
|
|
198
202
|
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
package/appsscript.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"oauthScopes": [
|
|
6
6
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
7
7
|
"https://www.googleapis.com/auth/drive",
|
|
8
|
+
"https://www.googleapis.com/auth/sqlservice",
|
|
8
9
|
"https://www.googleapis.com/auth/script.external_request",
|
|
9
10
|
"https://www.googleapis.com/auth/spreadsheets",
|
|
10
11
|
"https://www.googleapis.com/auth/userinfo.email",
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"@sindresorhus/is": "^7.2.0",
|
|
12
12
|
"acorn": "^8.16.0",
|
|
13
13
|
"adm-zip": "^0.5.16",
|
|
14
|
+
"child_process": "^1.0.2",
|
|
14
15
|
"commander": "^14.0.3",
|
|
15
16
|
"dotenv": "^17.3.1",
|
|
16
17
|
"fast-xml-parser": "^5.5.9",
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
"keyv": "^5.6.0",
|
|
23
24
|
"keyv-file": "^5.3.3",
|
|
24
25
|
"mime": "^4.1.0",
|
|
26
|
+
"mysql2": "^3.20.0",
|
|
27
|
+
"pg": "^8.20.0",
|
|
25
28
|
"prompts": "^2.4.2",
|
|
26
29
|
"sleep-synchronously": "^2.0.0",
|
|
27
30
|
"zod": "^4.3.6"
|
|
@@ -35,7 +38,7 @@
|
|
|
35
38
|
},
|
|
36
39
|
"name": "@mcpher/gas-fakes",
|
|
37
40
|
"author": "bruce mcpherson",
|
|
38
|
-
"version": "2.3.
|
|
41
|
+
"version": "2.3.5",
|
|
39
42
|
"license": "MIT",
|
|
40
43
|
"main": "main.js",
|
|
41
44
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
package/skills-lock.json
ADDED
package/src/cli/app.js
CHANGED
|
@@ -175,6 +175,24 @@ export async function main() {
|
|
|
175
175
|
.option("-t, --tools <string>", "Path to custom tools file.")
|
|
176
176
|
.action(startMcpServer);
|
|
177
177
|
|
|
178
|
+
// --- JDBC Command ---
|
|
179
|
+
program
|
|
180
|
+
.command("jdbc")
|
|
181
|
+
.description("Parse a JDBC connection string and output configurations for App Script and Local environments.")
|
|
182
|
+
.requiredOption("-c, --connection-string <string>", "The JDBC connection string to parse.")
|
|
183
|
+
.action(async (options) => {
|
|
184
|
+
try {
|
|
185
|
+
const { newFakeJdbcService } = await import("../services/jdbc/fakejdbcservice.js");
|
|
186
|
+
const service = newFakeJdbcService();
|
|
187
|
+
const config = service.__normalConnection(options.connectionString);
|
|
188
|
+
process.stdout.write(JSON.stringify(config, null, 2) + "\n");
|
|
189
|
+
process.exit(0);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
process.stderr.write(`Error parsing connection string: ${err.message}\n`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
178
196
|
program.showHelpAfterError("(add --help for additional information)");
|
|
179
197
|
|
|
180
198
|
await program.parseAsync(process.argv);
|
package/src/cli/setup.js
CHANGED
|
@@ -242,16 +242,60 @@ export async function initializeConfiguration(options = {}) {
|
|
|
242
242
|
// Discover Scopes from appsscript.json (Shared across backends)
|
|
243
243
|
const manifestPath = path.resolve(process.cwd(), responses.GF_MANIFEST_PATH);
|
|
244
244
|
let manifestScopes = [];
|
|
245
|
+
let manifestHasCloudSql = false;
|
|
245
246
|
if (fs.existsSync(manifestPath)) {
|
|
246
247
|
try {
|
|
247
248
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
248
249
|
manifestScopes = manifest.oauthScopes || [];
|
|
249
250
|
console.log(`...discovered ${manifestScopes.length} scopes in ${responses.GF_MANIFEST_PATH}`);
|
|
251
|
+
|
|
252
|
+
// Check for JDBC or Cloud SQL usage (simplified check)
|
|
253
|
+
const manifestContent = fs.readFileSync(manifestPath, "utf8");
|
|
254
|
+
if (manifestContent.includes("jdbc:google:") || manifestContent.includes("Jdbc.")) {
|
|
255
|
+
manifestHasCloudSql = true;
|
|
256
|
+
}
|
|
250
257
|
} catch (err) {
|
|
251
258
|
console.warn(`...warning: failed to parse ${responses.GF_MANIFEST_PATH}.`);
|
|
252
259
|
}
|
|
253
260
|
}
|
|
254
261
|
|
|
262
|
+
// --- Step 2.5: Database & Cloud SQL Proxy Configuration ---
|
|
263
|
+
const hasJdbc = manifestHasCloudSql || Object.keys(existingConfig).some(k => k.includes("DATABASE_URL"));
|
|
264
|
+
if (hasJdbc) {
|
|
265
|
+
console.log("\n--- Configuring Database & Cloud SQL Auth Proxy ---");
|
|
266
|
+
|
|
267
|
+
// Check if proxy is installed
|
|
268
|
+
let proxyInstalled = false;
|
|
269
|
+
try {
|
|
270
|
+
execSync("cloud-sql-proxy --version", { stdio: "ignore" });
|
|
271
|
+
proxyInstalled = true;
|
|
272
|
+
} catch (e) {
|
|
273
|
+
// not installed or not in path
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!proxyInstalled) {
|
|
277
|
+
console.log("\x1b[1;33mNotice: Cloud SQL Auth Proxy is not installed or not in your PATH.\x1b[0m");
|
|
278
|
+
console.log("If you plan to test Google Cloud SQL locally, please install it:");
|
|
279
|
+
console.log(" - macOS (Homebrew): brew install google-cloud-sdk");
|
|
280
|
+
console.log(" then: gcloud components install cloud-sql-proxy");
|
|
281
|
+
console.log(" - Other: https://cloud.google.com/sql/docs/postgres/sql-proxy#install\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const dbQuestions = [
|
|
285
|
+
{
|
|
286
|
+
type: "toggle",
|
|
287
|
+
name: "GF_USE_CLOUD_PG_SQL_PROXY",
|
|
288
|
+
message: "Use Cloud SQL Auth Proxy for local database connections?",
|
|
289
|
+
initial: existingConfig.GF_USE_CLOUD_PG_SQL_PROXY === "true",
|
|
290
|
+
active: "yes",
|
|
291
|
+
inactive: "no"
|
|
292
|
+
}
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const dbResponses = await prompts(dbQuestions);
|
|
296
|
+
Object.assign(responses, dbResponses);
|
|
297
|
+
}
|
|
298
|
+
|
|
255
299
|
// --- Step 3: Google Workspace Configuration ---
|
|
256
300
|
if (platforms.includes("google")) {
|
|
257
301
|
console.log("\n--- Configuring Google Workspace backend ---");
|
|
@@ -756,8 +800,8 @@ export async function authenticateUser(options = {}) {
|
|
|
756
800
|
runCommandSync(`gcloud iam service-accounts add-iam-policy-binding "${sa_email}" --member="user:${current_user}" --role="roles/iam.serviceAccountTokenCreator" --quiet`, true);
|
|
757
801
|
|
|
758
802
|
const saUniqueId = execSync(`gcloud iam service-accounts describe "${sa_email}" --format="value(uniqueId)"`, { shell: true }).toString().trim();
|
|
759
|
-
console.log(`\n\x1b[1;33m
|
|
760
|
-
console.log(`IMPORTANT:
|
|
803
|
+
console.log(`\n\x1b[1;33m*************************************************************************************************`);
|
|
804
|
+
console.log(`IMPORTANT: If you haven't already done it, add this to Admin Console (Domain-Wide Delegation):`);
|
|
761
805
|
console.log(`************************************************************************\x1b[0m`);
|
|
762
806
|
console.log(`URL: https://admin.google.com/ac/owl/domainwidedelegation`);
|
|
763
807
|
console.log(`Client ID: ${saUniqueId}\nScopes: ${scopes}`);
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* the idea here is to create an empty global entry for the singleton
|
|
3
|
+
* but only load it when it is actually used.
|
|
4
|
+
*/
|
|
5
|
+
import { newFakeJdbcService as maker } from './fakejdbcservice.js';
|
|
6
|
+
import { lazyLoaderApp } from '../common/lazyloader.js'
|
|
7
|
+
|
|
8
|
+
let _app = null;
|
|
9
|
+
_app = lazyLoaderApp(_app, 'Jdbc', maker);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { Syncit } from '../../support/syncit.js';
|
|
3
|
+
import { newFakeJdbcStatement } from './fakejdbcstatement.js';
|
|
4
|
+
|
|
5
|
+
class FakeJdbcConnection {
|
|
6
|
+
constructor(url, user, password) {
|
|
7
|
+
this.__fakeObjectType = 'JdbcConnection';
|
|
8
|
+
this._url = url;
|
|
9
|
+
|
|
10
|
+
// Connect synchronously using the worker
|
|
11
|
+
// Only pass arguments that are provided to avoid passing null/undefined to Syncit
|
|
12
|
+
const args = [url];
|
|
13
|
+
if (user !== null && typeof user !== 'undefined') args.push(user);
|
|
14
|
+
if (password !== null && typeof password !== 'undefined') args.push(password);
|
|
15
|
+
|
|
16
|
+
const result = Syncit.fxJdbcConnect(...args);
|
|
17
|
+
this._connectionId = result.id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
createStatement() {
|
|
21
|
+
return newFakeJdbcStatement(this._connectionId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getMetaData() {
|
|
25
|
+
// Return an object that mimics DatabaseMetaData
|
|
26
|
+
return Proxies.guard({
|
|
27
|
+
__fakeObjectType: 'JdbcDatabaseMetaData',
|
|
28
|
+
getURL: () => this._url
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// To match GAS JdbcConnection basic capabilities
|
|
33
|
+
close() {
|
|
34
|
+
if (this._connectionId) {
|
|
35
|
+
Syncit.fxJdbcClose(this._connectionId);
|
|
36
|
+
this._connectionId = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isClosed() {
|
|
41
|
+
return !this._connectionId;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const newFakeJdbcConnection = (...args) => Proxies.guard(new FakeJdbcConnection(...args));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeJdbcResultSetMetaData } from './fakejdbcresultsetmetadata.js';
|
|
3
|
+
|
|
4
|
+
class FakeJdbcResultSet {
|
|
5
|
+
constructor(result) {
|
|
6
|
+
this.__fakeObjectType = 'JdbcResultSet';
|
|
7
|
+
this._rows = result.rows || [];
|
|
8
|
+
this._fields = result.fields || [];
|
|
9
|
+
this._currentIndex = -1;
|
|
10
|
+
this._isClosed = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
next() {
|
|
14
|
+
if (this._isClosed) throw new Error('ResultSet is closed.');
|
|
15
|
+
this._currentIndex++;
|
|
16
|
+
return this._currentIndex < this._rows.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_getValue(columnIndex) {
|
|
20
|
+
if (this._isClosed) throw new Error('ResultSet is closed.');
|
|
21
|
+
if (this._currentIndex < 0 || this._currentIndex >= this._rows.length) {
|
|
22
|
+
throw new Error('No current row.');
|
|
23
|
+
}
|
|
24
|
+
// JDBC columns are 1-indexed
|
|
25
|
+
const field = this._fields[columnIndex - 1];
|
|
26
|
+
if (!field) throw new Error(`Invalid column index: ${columnIndex}`);
|
|
27
|
+
return this._rows[this._currentIndex][field.name];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getString(columnIndex) {
|
|
31
|
+
const val = this._getValue(columnIndex);
|
|
32
|
+
return val !== null && val !== undefined ? String(val) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getInt(columnIndex) {
|
|
36
|
+
const val = this._getValue(columnIndex);
|
|
37
|
+
return val !== null && val !== undefined ? parseInt(val, 10) : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getFloat(columnIndex) {
|
|
41
|
+
const val = this._getValue(columnIndex);
|
|
42
|
+
return val !== null && val !== undefined ? parseFloat(val) : 0.0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getDate(columnIndex) {
|
|
46
|
+
const val = this._getValue(columnIndex);
|
|
47
|
+
return val ? new Date(val) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getObject(columnIndex) {
|
|
51
|
+
return this._getValue(columnIndex);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getMetaData() {
|
|
55
|
+
if (this._isClosed) throw new Error('ResultSet is closed.');
|
|
56
|
+
return newFakeJdbcResultSetMetaData(this._fields);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
this._isClosed = true;
|
|
61
|
+
this._rows = [];
|
|
62
|
+
this._fields = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isClosed() {
|
|
66
|
+
return this._isClosed;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const newFakeJdbcResultSet = (...args) => Proxies.guard(new FakeJdbcResultSet(...args));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
|
|
3
|
+
class FakeJdbcResultSetMetaData {
|
|
4
|
+
constructor(fields) {
|
|
5
|
+
this.__fakeObjectType = 'JdbcResultSetMetaData';
|
|
6
|
+
this._fields = fields || [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
getColumnCount() {
|
|
10
|
+
return this._fields.length;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getColumnName(column) {
|
|
14
|
+
const field = this._fields[column - 1];
|
|
15
|
+
if (!field) throw new Error(`Invalid column index: ${column}`);
|
|
16
|
+
return field.name;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getColumnLabel(column) {
|
|
20
|
+
return this.getColumnName(column);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getColumnType(column) {
|
|
24
|
+
// Return approximate JDBC type from pg dataTypeID
|
|
25
|
+
const field = this._fields[column - 1];
|
|
26
|
+
if (!field) throw new Error(`Invalid column index: ${column}`);
|
|
27
|
+
// node-postgres gives type ids (OIDs) like 23 for INT4, 25 for TEXT
|
|
28
|
+
// Usually JDBC maps them to java.sql.Types integers. For a fake, returning the OID or a mock type integer is adequate for basic use cases.
|
|
29
|
+
return field.dataTypeID;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getColumnTypeName(column) {
|
|
33
|
+
const field = this._fields[column - 1];
|
|
34
|
+
if (!field) throw new Error(`Invalid column index: ${column}`);
|
|
35
|
+
return `TYPE_${field.dataTypeID}`; // basic fallback
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const newFakeJdbcResultSetMetaData = (...args) => Proxies.guard(new FakeJdbcResultSetMetaData(...args));
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeJdbcConnection } from './fakejdbcconnection.js';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
class FakeJdbcService {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.__fakeObjectType = 'Jdbc';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Connects to a JDBC database.
|
|
12
|
+
* In gas-fakes, we primarily support postgres via pg module, mapping jdbc:postgresql to it.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} url The URL of the database to connect to.
|
|
15
|
+
* @param {string} user (optional) The user name to connect as.
|
|
16
|
+
* @param {string} password (optional) The password for the user.
|
|
17
|
+
* @returns {JdbcConnection} A JDBC connection object.
|
|
18
|
+
*/
|
|
19
|
+
getConnection(url, user, password) {
|
|
20
|
+
let finalUrl = url;
|
|
21
|
+
|
|
22
|
+
if (!finalUrl) {
|
|
23
|
+
throw new Error('Jdbc.getConnection: URL is required or DATABASE_URL must be set in environment');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Explicitly merge user/password into the URL structure for gas-fakes processing
|
|
27
|
+
if (user !== undefined && user !== null && password !== undefined && password !== null) {
|
|
28
|
+
try {
|
|
29
|
+
const cleanUrl = finalUrl.replace(/^jdbc:google:/, '').replace(/^jdbc:/, '');
|
|
30
|
+
const urlObj = new URL(cleanUrl);
|
|
31
|
+
urlObj.username = encodeURIComponent(String(user));
|
|
32
|
+
urlObj.password = encodeURIComponent(String(password));
|
|
33
|
+
|
|
34
|
+
// Restore the prefix
|
|
35
|
+
let prefix = "jdbc:";
|
|
36
|
+
if (finalUrl.startsWith("jdbc:google:")) prefix = "jdbc:google:";
|
|
37
|
+
finalUrl = prefix + urlObj.toString();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Fallback for malformed URLs
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// We intentionally pass undefined for user and password so the worker only sees finalUrl
|
|
44
|
+
return newFakeJdbcConnection(finalUrl, undefined, undefined);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Connects to a Google Cloud SQL instance - but on gas-fakes its the same thing
|
|
49
|
+
* @param {string} url The URL of the database to connect to.
|
|
50
|
+
* @param {string} user (optional) The user name to connect as.
|
|
51
|
+
* @param {string} password (optional) The password for the user.
|
|
52
|
+
* @returns {JdbcConnection} A JDBC connection object.
|
|
53
|
+
*/
|
|
54
|
+
getCloudSqlConnection(url, user, password) {
|
|
55
|
+
return this.getConnection (url, user, password)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
parseCsv(csv) {
|
|
59
|
+
throw new Error('Not implemented: parseCsv');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
__useProxy (val) {
|
|
63
|
+
if (!val) return false;
|
|
64
|
+
const normal = this.__normalConnection (val)
|
|
65
|
+
return normal.local.useProxy;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Converts Database URLs to JDBC-compatible formats for testing and local proxy usage.
|
|
70
|
+
* @param {string} url - Format: protocol://user:pass@host/db OR protocol://host/db?user=X&password=Y
|
|
71
|
+
* @return {object} - Connection metadata.
|
|
72
|
+
*/
|
|
73
|
+
__normalConnection(url) {
|
|
74
|
+
const isProxyRunning = (instanceName) => {
|
|
75
|
+
try {
|
|
76
|
+
execSync(`pgrep -f "cloud.*sql.*proxy.*${instanceName}"`, {
|
|
77
|
+
stdio: "ignore",
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const aggressiveEncode = (str) => {
|
|
86
|
+
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
|
|
87
|
+
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const cleanUrl = url.replace(/^jdbc:google:/, "").replace(/^jdbc:/, "");
|
|
92
|
+
|
|
93
|
+
let scheme, user, pass, host, db;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const parsed = new URL(cleanUrl);
|
|
97
|
+
scheme = parsed.protocol.replace(':', '');
|
|
98
|
+
user = parsed.username || "";
|
|
99
|
+
pass = parsed.password || "";
|
|
100
|
+
host = parsed.hostname;
|
|
101
|
+
const portFromUrl = parsed.port;
|
|
102
|
+
if (portFromUrl) {
|
|
103
|
+
host = `${host}:${portFromUrl}`;
|
|
104
|
+
}
|
|
105
|
+
db = parsed.pathname.replace(/^\//, '');
|
|
106
|
+
|
|
107
|
+
const remainingParams = new URLSearchParams(parsed.search);
|
|
108
|
+
if (!user && remainingParams.has('user')) user = remainingParams.get('user');
|
|
109
|
+
if (!pass && remainingParams.has('password')) pass = remainingParams.get('password');
|
|
110
|
+
remainingParams.delete('user');
|
|
111
|
+
remainingParams.delete('password');
|
|
112
|
+
|
|
113
|
+
const searchString = remainingParams.toString();
|
|
114
|
+
if (searchString) {
|
|
115
|
+
db = `${db}?${searchString}`;
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
const regex = /^([^:]+):\/\/(.+):([^@]+)@([^/]+)(?:\/(.*))?/;
|
|
119
|
+
const match = cleanUrl.match(regex);
|
|
120
|
+
if (!match) throw new Error(`Format not recognized for ${cleanUrl}`);
|
|
121
|
+
scheme = match[1].trim();
|
|
122
|
+
user = match[2].trim();
|
|
123
|
+
pass = match[3].trim();
|
|
124
|
+
host = match[4].trim();
|
|
125
|
+
db = match[5] ? match[5].trim() : "postgres";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
user = decodeURIComponent(user.trim());
|
|
129
|
+
pass = decodeURIComponent(pass.trim());
|
|
130
|
+
host = host.trim();
|
|
131
|
+
|
|
132
|
+
let pureDb = db ? db.trim() : "postgres";
|
|
133
|
+
if (pureDb.includes('?')) pureDb = pureDb.split('?')[0];
|
|
134
|
+
|
|
135
|
+
const isPostgres = scheme.toLowerCase().includes("post");
|
|
136
|
+
const type = isPostgres ? "pg" : "mysql";
|
|
137
|
+
const protocol = isPostgres ? "jdbc:postgresql://" : "jdbc:mysql://";
|
|
138
|
+
const defaultPort = isPostgres ? ":5432" : ":3306";
|
|
139
|
+
|
|
140
|
+
let hostWithoutPort = host.replace(/:\d+$/, "");
|
|
141
|
+
let isCloudSql = hostWithoutPort.includes(":");
|
|
142
|
+
let isGoogle = isCloudSql || url.startsWith('jdbc:google:');
|
|
143
|
+
let useProxy = false;
|
|
144
|
+
let remoteHost = host;
|
|
145
|
+
|
|
146
|
+
const cloudSqlInstanceName = hostWithoutPort;
|
|
147
|
+
|
|
148
|
+
if (isCloudSql) {
|
|
149
|
+
try {
|
|
150
|
+
const instanceParts = hostWithoutPort.split(":");
|
|
151
|
+
const instanceName = instanceParts[instanceParts.length - 1];
|
|
152
|
+
useProxy = isProxyRunning(instanceName);
|
|
153
|
+
|
|
154
|
+
const instanceInfoStr = execSync(`gcloud sql instances describe ${instanceName} --format=json`, { encoding: "utf-8", stdio: "pipe" });
|
|
155
|
+
const instanceInfo = JSON.parse(instanceInfoStr);
|
|
156
|
+
const primaryIpObj = instanceInfo.ipAddresses?.find((ip) => ip.type === "PRIMARY");
|
|
157
|
+
const ip = primaryIpObj ? primaryIpObj.ipAddress : null;
|
|
158
|
+
|
|
159
|
+
if (ip && ip.match(/^[0-9.]+$/)) {
|
|
160
|
+
remoteHost = ip;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!useProxy) {
|
|
164
|
+
const localIp = execSync("curl -s https://ifconfig.me", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
165
|
+
if (localIp && localIp.match(/^[0-9.]+$/)) {
|
|
166
|
+
const authorizedNetworks = instanceInfo.settings?.ipConfiguration?.authorizedNetworks || [];
|
|
167
|
+
if (!authorizedNetworks.some((net) => net.value === localIp || net.value === `${localIp}/32`)) {
|
|
168
|
+
const newNetworks = authorizedNetworks.map((n) => n.value).concat(`${localIp}/32`).join(",");
|
|
169
|
+
execSync(`gcloud sql instances patch ${instanceName} --authorized-networks="${newNetworks}" --quiet`, { encoding: "utf-8", stdio: "pipe" });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!remoteHost.includes(":")) remoteHost += defaultPort;
|
|
177
|
+
|
|
178
|
+
const encodedUser = aggressiveEncode(user);
|
|
179
|
+
const encodedPass = aggressiveEncode(pass);
|
|
180
|
+
|
|
181
|
+
// Live Apps Script Java JDBC requires properly encoded URI components to prevent "Connection URL is malformed"
|
|
182
|
+
const gasAuthQuery = (encodedUser || encodedPass) ? `?user=${encodedUser}&password=${encodedPass}` : '';
|
|
183
|
+
|
|
184
|
+
// --- GAS ---
|
|
185
|
+
let gasUrl, gasConnectionString, gasFullConnectionString;
|
|
186
|
+
let isCloudSqlConnection = false;
|
|
187
|
+
|
|
188
|
+
if (isGoogle && !isPostgres) {
|
|
189
|
+
gasUrl = `jdbc:google:mysql://${cloudSqlInstanceName}/${pureDb}`;
|
|
190
|
+
gasConnectionString = gasUrl;
|
|
191
|
+
gasFullConnectionString = gasUrl;
|
|
192
|
+
isCloudSqlConnection = true;
|
|
193
|
+
} else {
|
|
194
|
+
gasUrl = `${protocol}${remoteHost}/${pureDb}`;
|
|
195
|
+
gasConnectionString = `${protocol}${remoteHost}/${pureDb}`;
|
|
196
|
+
// Drop all original query parameters, only append explicit unencoded user, password, and ssl
|
|
197
|
+
gasFullConnectionString = `${protocol}${remoteHost}/${pureDb}${gasAuthQuery}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- LOCAL ---
|
|
201
|
+
let localHostAddr = useProxy ? `127.0.0.1${defaultPort}` : remoteHost;
|
|
202
|
+
let localSslParam = useProxy ? "ssl=false" : "";
|
|
203
|
+
let localAuthParams = (encodedUser || encodedPass) ? `user=${encodedUser}&password=${encodedPass}` : '';
|
|
204
|
+
|
|
205
|
+
// Add an ampersand if both auth params and ssl param exist
|
|
206
|
+
if (localAuthParams && localSslParam) {
|
|
207
|
+
localAuthParams += "&";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let localUrl = `${protocol}${localHostAddr}/${pureDb}`;
|
|
211
|
+
let localQuery = `${localAuthParams}${localSslParam}`;
|
|
212
|
+
let localConnectionString = `${protocol}${localHostAddr}/${pureDb}${localQuery ? '?' + localQuery : ''}`;
|
|
213
|
+
let localFullConnectionString = localConnectionString;
|
|
214
|
+
|
|
215
|
+
const proxyCommand = (isCloudSql || isGoogle) && cloudSqlInstanceName.includes(":") ? `cloud-sql-proxy ${cloudSqlInstanceName}` : "";
|
|
216
|
+
|
|
217
|
+
const gasObj = {
|
|
218
|
+
url: gasUrl,
|
|
219
|
+
connectionString: gasConnectionString,
|
|
220
|
+
user, password: pass,
|
|
221
|
+
fullConnectionString: gasFullConnectionString,
|
|
222
|
+
isCloudSqlConnection,
|
|
223
|
+
proxyCommand
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const localObj = {
|
|
227
|
+
url: localUrl,
|
|
228
|
+
connectionString: localConnectionString,
|
|
229
|
+
user, password: pass,
|
|
230
|
+
fullConnectionString: localFullConnectionString,
|
|
231
|
+
useProxy, proxyCommand
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
current: (typeof ScriptApp !== 'undefined' && ScriptApp.isFake) ? 'local' : 'gas',
|
|
236
|
+
gas: gasObj,
|
|
237
|
+
local: localObj,
|
|
238
|
+
host: remoteHost,
|
|
239
|
+
proxyCommand,
|
|
240
|
+
isGoogle, type
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const newFakeJdbcService = (...args) => Proxies.guard(new FakeJdbcService(...args));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { Syncit } from '../../support/syncit.js';
|
|
3
|
+
import { newFakeJdbcResultSet } from './fakejdbcresultset.js';
|
|
4
|
+
|
|
5
|
+
class FakeJdbcStatement {
|
|
6
|
+
constructor(connectionId) {
|
|
7
|
+
this.__fakeObjectType = 'JdbcStatement';
|
|
8
|
+
this._connectionId = connectionId;
|
|
9
|
+
this._isClosed = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
executeQuery(sql) {
|
|
13
|
+
if (this._isClosed) throw new Error('Statement is closed.');
|
|
14
|
+
|
|
15
|
+
// Fetch result synchronously from worker
|
|
16
|
+
const result = Syncit.fxJdbcQuery(this._connectionId, sql);
|
|
17
|
+
|
|
18
|
+
return newFakeJdbcResultSet(result);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
execute(sql) {
|
|
22
|
+
if (this._isClosed) throw new Error('Statement is closed.');
|
|
23
|
+
|
|
24
|
+
// In actual JDBC, execute returns true if the first result is a ResultSet
|
|
25
|
+
// node-postgres gives us an array of rows or a rowCount.
|
|
26
|
+
const result = Syncit.fxJdbcQuery(this._connectionId, sql);
|
|
27
|
+
return result.fields && result.fields.length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
close() {
|
|
31
|
+
this._isClosed = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isClosed() {
|
|
35
|
+
return this._isClosed;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const newFakeJdbcStatement = (...args) => Proxies.guard(new FakeJdbcStatement(...args));
|
|
@@ -40,6 +40,86 @@ export class FakeEmbeddedChart {
|
|
|
40
40
|
return newFakeContainerInfo(this.__apiChart.position?.overlayPosition);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Returns the ranges that this chart uses for its source data.
|
|
45
|
+
* @returns {FakeSheetRange[]}
|
|
46
|
+
*/
|
|
47
|
+
getRanges() {
|
|
48
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "EmbeddedChart.getRanges");
|
|
49
|
+
if (nargs !== 0) matchThrow();
|
|
50
|
+
|
|
51
|
+
const ranges = [];
|
|
52
|
+
const spec = this.__apiChart.spec;
|
|
53
|
+
|
|
54
|
+
const extractRanges = (chartData) => {
|
|
55
|
+
if (chartData?.sourceRange?.sources) {
|
|
56
|
+
chartData.sourceRange.sources.forEach((gridRange) => {
|
|
57
|
+
const sheet = this.__sheet.getParent().getSheetById(gridRange.sheetId);
|
|
58
|
+
if (sheet) {
|
|
59
|
+
const numRows = (gridRange.endRowIndex !== undefined ? gridRange.endRowIndex : sheet.getMaxRows()) - gridRange.startRowIndex;
|
|
60
|
+
const numCols = (gridRange.endColumnIndex !== undefined ? gridRange.endColumnIndex : sheet.getMaxColumns()) - gridRange.startColumnIndex;
|
|
61
|
+
ranges.push(
|
|
62
|
+
sheet.getRange(
|
|
63
|
+
gridRange.startRowIndex + 1,
|
|
64
|
+
gridRange.startColumnIndex + 1,
|
|
65
|
+
numRows,
|
|
66
|
+
numCols
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (spec.basicChart) {
|
|
75
|
+
spec.basicChart.domains?.forEach((d) => extractRanges(d.domain));
|
|
76
|
+
spec.basicChart.series?.forEach((s) => extractRanges(s.series));
|
|
77
|
+
}
|
|
78
|
+
if (spec.pieChart) {
|
|
79
|
+
extractRanges(spec.pieChart.domain);
|
|
80
|
+
spec.pieChart.series?.forEach((s) => extractRanges(s.series));
|
|
81
|
+
}
|
|
82
|
+
if (spec.bubbleChart) {
|
|
83
|
+
extractRanges(spec.bubbleChart.domain);
|
|
84
|
+
extractRanges(spec.bubbleChart.series);
|
|
85
|
+
extractRanges(spec.bubbleChart.ids);
|
|
86
|
+
extractRanges(spec.bubbleChart.labels);
|
|
87
|
+
extractRanges(spec.bubbleChart.sizes);
|
|
88
|
+
}
|
|
89
|
+
if (spec.candlestickChart) {
|
|
90
|
+
spec.candlestickChart.data?.forEach((d) => {
|
|
91
|
+
extractRanges(d.highSeries?.series);
|
|
92
|
+
extractRanges(d.lowSeries?.series);
|
|
93
|
+
extractRanges(d.openSeries?.series);
|
|
94
|
+
extractRanges(d.closeSeries?.series);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (spec.histogramChart) {
|
|
98
|
+
spec.histogramChart.series?.forEach((s) => extractRanges(s.data));
|
|
99
|
+
}
|
|
100
|
+
if (spec.orgChart) {
|
|
101
|
+
extractRanges(spec.orgChart.labels);
|
|
102
|
+
extractRanges(spec.orgChart.parentLabels);
|
|
103
|
+
extractRanges(spec.orgChart.tooltips);
|
|
104
|
+
}
|
|
105
|
+
if (spec.scorecardChart) {
|
|
106
|
+
extractRanges(spec.scorecardChart.keyValueData);
|
|
107
|
+
extractRanges(spec.scorecardChart.baselineValueData);
|
|
108
|
+
}
|
|
109
|
+
if (spec.treemapChart) {
|
|
110
|
+
extractRanges(spec.treemapChart.labels);
|
|
111
|
+
extractRanges(spec.treemapChart.parentLabels);
|
|
112
|
+
extractRanges(spec.treemapChart.sizeData);
|
|
113
|
+
extractRanges(spec.treemapChart.colorData);
|
|
114
|
+
}
|
|
115
|
+
if (spec.waterfallChart) {
|
|
116
|
+
extractRanges(spec.waterfallChart.domain?.domain);
|
|
117
|
+
spec.waterfallChart.series?.forEach((s) => extractRanges(s.data));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return ranges;
|
|
121
|
+
}
|
|
122
|
+
|
|
43
123
|
/**
|
|
44
124
|
* Returns the ID of this chart.
|
|
45
125
|
* @returns {number}
|
package/src/support/sxauth.js
CHANGED
|
@@ -128,6 +128,32 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
|
|
|
128
128
|
|
|
129
129
|
} catch (err) {
|
|
130
130
|
syncWarn(`Google authentication failed: ${err.message}`);
|
|
131
|
+
|
|
132
|
+
// Provide guidance for Domain Wide Delegation issues
|
|
133
|
+
if (err.message.includes('unauthorized_client')) {
|
|
134
|
+
const clientId = Auth.getClientId();
|
|
135
|
+
const msg = [
|
|
136
|
+
"",
|
|
137
|
+
"=".repeat(80),
|
|
138
|
+
"GOOGLE AUTHENTICATION ERROR: unauthorized_client",
|
|
139
|
+
"This usually means Domain-Wide Delegation (DWD) is missing for one or more scopes.",
|
|
140
|
+
"",
|
|
141
|
+
`Your Service Account Client ID is: ${clientId || 'unknown (check your service account JSON file)'}`,
|
|
142
|
+
"",
|
|
143
|
+
"The following scopes should be authorized in the Google Admin Console:",
|
|
144
|
+
finalScopes.join(","),
|
|
145
|
+
"",
|
|
146
|
+
"To fix this:",
|
|
147
|
+
"1. Go to https://admin.google.com",
|
|
148
|
+
"2. Security -> Access and data control -> API controls",
|
|
149
|
+
"3. Manage Domain Wide Delegation",
|
|
150
|
+
"4. Find/Add your Client ID and ensure the list of scopes above matches exactly.",
|
|
151
|
+
"=".repeat(80),
|
|
152
|
+
""
|
|
153
|
+
].join("\n");
|
|
154
|
+
console.error(msg);
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
if (!platforms.includes('ksuite') && !platforms.includes('msgraph')) throw err;
|
|
132
158
|
}
|
|
133
159
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import mysql from 'mysql2/promise';
|
|
3
|
+
|
|
4
|
+
const connections = new Map();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates and stores a connection to the database.
|
|
8
|
+
* @param {import('./auth.js').Auth} Auth
|
|
9
|
+
* @param {object} params
|
|
10
|
+
* @param {string} params.url JDBC connection URL
|
|
11
|
+
* @param {string} params.user Optional username
|
|
12
|
+
* @param {string} params.password Optional password
|
|
13
|
+
* @returns {object} Connection details/ID
|
|
14
|
+
*/
|
|
15
|
+
export const sxJdbcConnect = async (Auth, { url, user, password }) => {
|
|
16
|
+
let connectionString = url;
|
|
17
|
+
let type = 'pg';
|
|
18
|
+
|
|
19
|
+
if (url.startsWith('jdbc:postgresql:')) {
|
|
20
|
+
connectionString = url.replace('jdbc:postgresql:', 'postgresql:');
|
|
21
|
+
type = 'pg';
|
|
22
|
+
} else if (url.startsWith('jdbc:mysql:')) {
|
|
23
|
+
connectionString = url.replace('jdbc:mysql:', 'mysql:');
|
|
24
|
+
type = 'mysql';
|
|
25
|
+
} else if (url.startsWith('jdbc:google:mysql:')) {
|
|
26
|
+
connectionString = url.replace('jdbc:google:mysql:', 'mysql:');
|
|
27
|
+
type = 'mysql';
|
|
28
|
+
} else if (url.startsWith('mysql:')) {
|
|
29
|
+
type = 'mysql';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse custom flags
|
|
33
|
+
const disableSsl = connectionString.includes('ssl=false') || connectionString.includes('127.0.0.1');
|
|
34
|
+
|
|
35
|
+
let client;
|
|
36
|
+
const id = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
37
|
+
|
|
38
|
+
if (type === 'pg') {
|
|
39
|
+
// Strip ssl parameters to avoid conflict with explicit ssl object
|
|
40
|
+
let pgConnectionString = connectionString.replace(/([?&])ssl=[^&]*(&|$)/g, '$1').replace(/[?&]$/, '');
|
|
41
|
+
|
|
42
|
+
// Neon postgres usually requires ssl
|
|
43
|
+
if (pgConnectionString.includes('sslmode=require') && !pgConnectionString.includes('uselibpqcompat')) {
|
|
44
|
+
const separator = pgConnectionString.includes('?') ? '&' : '?';
|
|
45
|
+
pgConnectionString += `${separator}uselibpqcompat=true`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const clientConfig = {
|
|
49
|
+
connectionString: pgConnectionString,
|
|
50
|
+
ssl: disableSsl ? false : { rejectUnauthorized: false }
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (user !== null && typeof user !== 'undefined' && password !== null && typeof password !== 'undefined') {
|
|
54
|
+
client = new pg.Client({
|
|
55
|
+
...clientConfig,
|
|
56
|
+
user: String(user),
|
|
57
|
+
password: String(password)
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
client = new pg.Client(clientConfig);
|
|
61
|
+
}
|
|
62
|
+
await client.connect();
|
|
63
|
+
} else if (type === 'mysql') {
|
|
64
|
+
let cleanUrl = connectionString.replace(/^jdbc:/, '');
|
|
65
|
+
if (!cleanUrl.startsWith('mysql://')) {
|
|
66
|
+
cleanUrl = 'mysql://' + cleanUrl;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let host = '127.0.0.1';
|
|
70
|
+
let port = 3306;
|
|
71
|
+
let database = '';
|
|
72
|
+
let urlUser;
|
|
73
|
+
let urlPassword;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const parsedUrl = new URL(cleanUrl);
|
|
77
|
+
host = parsedUrl.hostname;
|
|
78
|
+
port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 3306;
|
|
79
|
+
database = parsedUrl.pathname.replace(/^\//, '');
|
|
80
|
+
urlUser = parsedUrl.username ? decodeURIComponent(parsedUrl.username) : parsedUrl.searchParams.get('user');
|
|
81
|
+
urlPassword = parsedUrl.password ? decodeURIComponent(parsedUrl.password) : parsedUrl.searchParams.get('password');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Fallback for weird formats (like Cloud SQL instance names without resolved IPs)
|
|
84
|
+
const match = cleanUrl.match(/^mysql:\/\/(?:([^:]+):([^@]+)@)?([^/]+?)(?::(\d+))?(?:\/([^?]+))?(?:\?(.*))?$/);
|
|
85
|
+
if (match) {
|
|
86
|
+
urlUser = match[1] ? decodeURIComponent(match[1]) : undefined;
|
|
87
|
+
urlPassword = match[2] ? decodeURIComponent(match[2]) : undefined;
|
|
88
|
+
host = match[3];
|
|
89
|
+
port = match[4] ? parseInt(match[4], 10) : 3306;
|
|
90
|
+
database = match[5] || '';
|
|
91
|
+
} else {
|
|
92
|
+
host = cleanUrl.replace(/^mysql:\/\//, '');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const finalUser = (user !== null && typeof user !== 'undefined') ? String(user) : urlUser;
|
|
97
|
+
const finalPassword = (password !== null && typeof password !== 'undefined') ? String(password) : urlPassword;
|
|
98
|
+
|
|
99
|
+
const mysqlConfig = {
|
|
100
|
+
host: host,
|
|
101
|
+
port: port,
|
|
102
|
+
database: database,
|
|
103
|
+
user: finalUser,
|
|
104
|
+
password: finalPassword,
|
|
105
|
+
ssl: disableSsl ? undefined : { rejectUnauthorized: false }
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
client = await mysql.createConnection(mysqlConfig);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
connections.set(id, { client, type });
|
|
112
|
+
return { id };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Executes a query on a given connection.
|
|
117
|
+
* @param {import('./auth.js').Auth} Auth
|
|
118
|
+
* @param {object} params
|
|
119
|
+
* @param {string} params.connectionId The stored connection ID
|
|
120
|
+
* @param {string} params.sql The SQL query to execute
|
|
121
|
+
* @returns {object} The query result (rows, fields, etc.)
|
|
122
|
+
*/
|
|
123
|
+
export const sxJdbcQuery = async (Auth, { connectionId, sql }) => {
|
|
124
|
+
const entry = connections.get(connectionId);
|
|
125
|
+
if (!entry) throw new Error('Invalid or closed JDBC connection.');
|
|
126
|
+
|
|
127
|
+
const { client, type } = entry;
|
|
128
|
+
|
|
129
|
+
if (type === 'pg') {
|
|
130
|
+
const result = await client.query(sql);
|
|
131
|
+
return {
|
|
132
|
+
rows: result.rows,
|
|
133
|
+
fields: result.fields,
|
|
134
|
+
rowCount: result.rowCount,
|
|
135
|
+
};
|
|
136
|
+
} else if (type === 'mysql') {
|
|
137
|
+
// mysql2 returns [rows, fields]
|
|
138
|
+
const [rows, fields] = await client.query(sql);
|
|
139
|
+
return {
|
|
140
|
+
rows: rows,
|
|
141
|
+
fields: fields, // mysql2 fields are objects with name, columnType, etc.
|
|
142
|
+
rowCount: rows.length || 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Closes a given connection.
|
|
149
|
+
* @param {import('./auth.js').Auth} Auth
|
|
150
|
+
* @param {object} params
|
|
151
|
+
* @param {string} params.connectionId The stored connection ID
|
|
152
|
+
* @returns {boolean} True if closed successfully
|
|
153
|
+
*/
|
|
154
|
+
export const sxJdbcClose = async (Auth, { connectionId }) => {
|
|
155
|
+
const entry = connections.get(connectionId);
|
|
156
|
+
if (entry) {
|
|
157
|
+
const { client, type } = entry;
|
|
158
|
+
if (type === 'pg') {
|
|
159
|
+
await client.end();
|
|
160
|
+
} else if (type === 'mysql') {
|
|
161
|
+
await client.end();
|
|
162
|
+
}
|
|
163
|
+
connections.delete(connectionId);
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
};
|
package/src/support/syncit.js
CHANGED
|
@@ -412,6 +412,21 @@ const fxTestRetry = (errorMessage) => {
|
|
|
412
412
|
return safeCallSync("sxTestRetry", { errorMessage });
|
|
413
413
|
};
|
|
414
414
|
|
|
415
|
+
const fxJdbcConnect = (url, user, password) => {
|
|
416
|
+
const args = { url };
|
|
417
|
+
if (user !== null && typeof user !== 'undefined') args.user = user;
|
|
418
|
+
if (password !== null && typeof password !== 'undefined') args.password = password;
|
|
419
|
+
return safeCallSync("sxJdbcConnect", args);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const fxJdbcQuery = (connectionId, sql) => {
|
|
423
|
+
return safeCallSync("sxJdbcQuery", { connectionId, sql });
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const fxJdbcClose = (connectionId) => {
|
|
427
|
+
return safeCallSync("sxJdbcClose", { connectionId });
|
|
428
|
+
};
|
|
429
|
+
|
|
415
430
|
const fxSheets = (args) =>
|
|
416
431
|
fxGeneric({
|
|
417
432
|
...args,
|
|
@@ -479,5 +494,8 @@ export const Syncit = {
|
|
|
479
494
|
fxGetAccessToken,
|
|
480
495
|
fxGetAccessTokenInfo,
|
|
481
496
|
fxGetSourceAccessTokenInfo,
|
|
482
|
-
fxTestRetry
|
|
497
|
+
fxTestRetry,
|
|
498
|
+
fxJdbcConnect,
|
|
499
|
+
fxJdbcQuery,
|
|
500
|
+
fxJdbcClose
|
|
483
501
|
}
|