@mcpher/gas-fakes 2.3.4 → 2.3.6

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 CHANGED
@@ -196,6 +196,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
196
196
  - [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
197
197
  - [running gas-fakes on Azure ACA](https://github.com/brucemcpherson/gas-fakes-containers)
198
198
  - [running gas-fakes on Github actions](https://github.com/brucemcpherson/gas-fakes-containers)
199
+ - [jdbc notes](jdbc-notes.md)
199
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/)
200
201
  - [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
201
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/main.js CHANGED
@@ -1,8 +1,6 @@
1
1
  // testing locally
2
2
  // sync the version with gas fakes code since they share a package.json
3
- import { createRequire } from 'node:module';
4
- const require = createRequire(import.meta.url);
5
- const pjson = require('./package.json');
6
- const VERSION = pjson.version;
7
- console.log (`...gas-fakes version ${VERSION}`)
3
+ import { initMetadata } from './src/support/metadata.js';
4
+ initMetadata();
5
+ console.log (`...gas-fakes version ${globalThis.GasFakes.metadata.version}`)
8
6
  import './src/index.js'
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.4",
41
+ "version": "2.3.6",
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",
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "neon-postgres": {
5
+ "source": "neondatabase/agent-skills",
6
+ "sourceType": "github",
7
+ "computedHash": "391693eee67001639ab65474df185da3ea5cf9ee4b99badecfd64561113edbb4"
8
+ }
9
+ }
10
+ }
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
@@ -5,9 +5,8 @@ import path from "path";
5
5
  import os from "os";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { execSync } from "child_process";
8
- import { checkForGcloudCli, checkForAzCli, runCommandSync } from "./utils.js";
8
+ import { checkForGcloudCli, runCommandSync } from "./utils.js";
9
9
  import { getMsGraphToken, mapGasScopesToMsGraph } from "../support/msgraph/msauth.js";
10
- import { Platforms, PlatformDefaults } from "../services/enums/platformenums.js";
11
10
 
12
11
  // --- Utility Functions ---
13
12
 
@@ -242,16 +241,60 @@ export async function initializeConfiguration(options = {}) {
242
241
  // Discover Scopes from appsscript.json (Shared across backends)
243
242
  const manifestPath = path.resolve(process.cwd(), responses.GF_MANIFEST_PATH);
244
243
  let manifestScopes = [];
244
+ let manifestHasCloudSql = false;
245
245
  if (fs.existsSync(manifestPath)) {
246
246
  try {
247
247
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
248
248
  manifestScopes = manifest.oauthScopes || [];
249
249
  console.log(`...discovered ${manifestScopes.length} scopes in ${responses.GF_MANIFEST_PATH}`);
250
+
251
+ // Check for JDBC or Cloud SQL usage (simplified check)
252
+ const manifestContent = fs.readFileSync(manifestPath, "utf8");
253
+ if (manifestContent.includes("jdbc:google:") || manifestContent.includes("Jdbc.")) {
254
+ manifestHasCloudSql = true;
255
+ }
250
256
  } catch (err) {
251
257
  console.warn(`...warning: failed to parse ${responses.GF_MANIFEST_PATH}.`);
252
258
  }
253
259
  }
254
260
 
261
+ // --- Step 2.5: Database & Cloud SQL Proxy Configuration ---
262
+ const hasJdbc = manifestHasCloudSql || Object.keys(existingConfig).some(k => k.includes("DATABASE_URL"));
263
+ if (hasJdbc) {
264
+ console.log("\n--- Configuring Database & Cloud SQL Auth Proxy ---");
265
+
266
+ // Check if proxy is installed
267
+ let proxyInstalled = false;
268
+ try {
269
+ execSync("cloud-sql-proxy --version", { stdio: "ignore" });
270
+ proxyInstalled = true;
271
+ } catch (e) {
272
+ // not installed or not in path
273
+ }
274
+
275
+ if (!proxyInstalled) {
276
+ console.log("\x1b[1;33mNotice: Cloud SQL Auth Proxy is not installed or not in your PATH.\x1b[0m");
277
+ console.log("If you plan to test Google Cloud SQL locally, please install it:");
278
+ console.log(" - macOS (Homebrew): brew install google-cloud-sdk");
279
+ console.log(" then: gcloud components install cloud-sql-proxy");
280
+ console.log(" - Other: https://cloud.google.com/sql/docs/postgres/sql-proxy#install\n");
281
+ }
282
+
283
+ const dbQuestions = [
284
+ {
285
+ type: "toggle",
286
+ name: "GF_USE_CLOUD_PG_SQL_PROXY",
287
+ message: "Use Cloud SQL Auth Proxy for local database connections?",
288
+ initial: existingConfig.GF_USE_CLOUD_PG_SQL_PROXY === "true",
289
+ active: "yes",
290
+ inactive: "no"
291
+ }
292
+ ];
293
+
294
+ const dbResponses = await prompts(dbQuestions);
295
+ Object.assign(responses, dbResponses);
296
+ }
297
+
255
298
  // --- Step 3: Google Workspace Configuration ---
256
299
  if (platforms.includes("google")) {
257
300
  console.log("\n--- Configuring Google Workspace backend ---");
@@ -756,8 +799,8 @@ export async function authenticateUser(options = {}) {
756
799
  runCommandSync(`gcloud iam service-accounts add-iam-policy-binding "${sa_email}" --member="user:${current_user}" --role="roles/iam.serviceAccountTokenCreator" --quiet`, true);
757
800
 
758
801
  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: Add this to Admin Console (Domain-Wide Delegation):`);
802
+ console.log(`\n\x1b[1;33m*************************************************************************************************`);
803
+ console.log(`IMPORTANT: If you haven't already done it, add this to Admin Console (Domain-Wide Delegation):`);
761
804
  console.log(`************************************************************************\x1b[0m`);
762
805
  console.log(`URL: https://admin.google.com/ac/owl/domainwidedelegation`);
763
806
  console.log(`Client ID: ${saUniqueId}\nScopes: ${scopes}`);
package/src/cli/utils.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { spawn, execSync } from "child_process";
2
- import { createRequire } from "node:module";
3
- const require = createRequire(import.meta.url);
4
- const pjson = require("../../package.json");
2
+ import { initMetadata } from "../support/metadata.js";
3
+ const GasFakes = initMetadata();
5
4
 
6
- export const VERSION = pjson.version;
5
+ export const VERSION = GasFakes.metadata.version;
7
6
  export const CLI_VERSION = "0.0.20";
8
7
  export const MCP_VERSION = "0.0.7";
9
8
 
package/src/index.js CHANGED
@@ -28,3 +28,5 @@ import './services/slidesapp/app.js'
28
28
  import './services/mimetype/app.js'
29
29
  import './services/lock/app.js'
30
30
  import './services/libhandlerapp/app.js'
31
+ import './services/jdbc/app.js'
32
+
@@ -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,37 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+
3
+ class FakeJdbcArray {
4
+ constructor(data, baseType, baseTypeName) {
5
+ this.__fakeObjectType = 'JdbcArray';
6
+ this._data = Array.isArray(data) ? data : [data];
7
+ this._baseType = baseType || 12; // VARCHAR
8
+ this._baseTypeName = baseTypeName || 'VARCHAR';
9
+ this._isClosed = false;
10
+ }
11
+
12
+ getArray() {
13
+ if (this._isClosed) throw new Error('Array is closed.');
14
+ return this._data;
15
+ }
16
+
17
+ getBaseType() {
18
+ return this._baseType;
19
+ }
20
+
21
+ getBaseTypeName() {
22
+ return this._baseTypeName;
23
+ }
24
+
25
+ getResultSet() {
26
+ // In actual JDBC this returns a result set with columns INDEX and VALUE
27
+ // For now, returning null as it's rarely used in Apps Script JDBC context
28
+ return null;
29
+ }
30
+
31
+ free() {
32
+ this._isClosed = true;
33
+ this._data = null;
34
+ }
35
+ }
36
+
37
+ export const newFakeJdbcArray = (...args) => Proxies.guard(new FakeJdbcArray(...args));
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Fake implementation of Google Apps Script Jdbc's BigDecimal (java.math.BigDecimal proxy)
3
+ */
4
+ export class FakeJdbcBigDecimal {
5
+ constructor(val) {
6
+ this.val = String(val);
7
+ }
8
+
9
+ /**
10
+ * Returns the string representation of this BigDecimal.
11
+ * This is how it's typically used in Apps Script for comparison or output.
12
+ */
13
+ toString() {
14
+ return this.val;
15
+ }
16
+
17
+ /**
18
+ * Returns the value as a JavaScript number.
19
+ * Corresponds to Java's doubleValue()
20
+ */
21
+ doubleValue() {
22
+ return Number(this.val);
23
+ }
24
+
25
+ /**
26
+ * Returns the value as an integer.
27
+ * Corresponds to Java's intValue()
28
+ */
29
+ intValue() {
30
+ return Math.floor(Number(this.val));
31
+ }
32
+
33
+ /**
34
+ * Returns the value as a long integer.
35
+ * Corresponds to Java's longValue()
36
+ */
37
+ longValue() {
38
+ return Math.floor(Number(this.val));
39
+ }
40
+
41
+ /**
42
+ * Returns the string representation without scientific notation.
43
+ * Corresponds to Java's toPlainString()
44
+ */
45
+ toPlainString() {
46
+ return this.val;
47
+ }
48
+
49
+ /**
50
+ * Returns the scale of this BigDecimal.
51
+ */
52
+ scale() {
53
+ const parts = this.val.split('.');
54
+ return parts.length > 1 ? parts[1].length : 0;
55
+ }
56
+
57
+ /**
58
+ * Returns the precision of this BigDecimal.
59
+ */
60
+ precision() {
61
+ return this.val.replace('.', '').replace('-', '').length;
62
+ }
63
+ }
@@ -0,0 +1,67 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeInputStream } from '../../support/fakeinputstream.js';
3
+
4
+ class FakeJdbcBlob {
5
+ constructor(data) {
6
+ this.__fakeObjectType = 'JdbcBlob';
7
+ // Ensure we have a Buffer/Uint8Array
8
+ this._data = (data instanceof Uint8Array || Buffer.isBuffer(data))
9
+ ? data
10
+ : Buffer.from(data || '');
11
+ this._isClosed = false;
12
+ }
13
+
14
+ getBinaryStream() {
15
+ if (this._isClosed) throw new Error('Blob is closed.');
16
+ return newFakeInputStream(this._data);
17
+ }
18
+
19
+ getBytes(pos, len) {
20
+ if (this._isClosed) throw new Error('Blob is closed.');
21
+ // pos is 1-indexed in JDBC
22
+ const start = pos - 1;
23
+ const end = start + len;
24
+ return Array.from(this._data.slice(start, end));
25
+ }
26
+
27
+ length() {
28
+ if (this._isClosed) throw new Error('Blob is closed.');
29
+ return this._data.length;
30
+ }
31
+
32
+ position(pattern, start) {
33
+ if (this._isClosed) throw new Error('Blob is closed.');
34
+ // Simple implementation for parity
35
+ const patternBuffer = (pattern instanceof FakeJdbcBlob) ? pattern._data : Buffer.from(pattern);
36
+ const index = this._data.indexOf(patternBuffer, start - 1);
37
+ return index === -1 ? -1 : index + 1;
38
+ }
39
+
40
+ setBinaryStream(pos) {
41
+ // Not fully supported in fake, but allows setting direction
42
+ return this.getBinaryStream();
43
+ }
44
+
45
+ setBytes(pos, bytes) {
46
+ if (this._isClosed) throw new Error('Blob is closed.');
47
+ const start = pos - 1;
48
+ const newBytes = Buffer.from(bytes);
49
+ const newData = Buffer.alloc(Math.max(this._data.length, start + newBytes.length));
50
+ this._data.copy(newData);
51
+ newBytes.copy(newData, start);
52
+ this._data = newData;
53
+ return newBytes.length;
54
+ }
55
+
56
+ truncate(len) {
57
+ if (this._isClosed) throw new Error('Blob is closed.');
58
+ this._data = this._data.slice(0, len);
59
+ }
60
+
61
+ free() {
62
+ this._isClosed = true;
63
+ this._data = null;
64
+ }
65
+ }
66
+
67
+ export const newFakeJdbcBlob = (...args) => Proxies.guard(new FakeJdbcBlob(...args));
@@ -0,0 +1,67 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeInputStream, newFakeReader } from '../../support/fakeinputstream.js';
3
+
4
+ class FakeJdbcClob {
5
+ constructor(data) {
6
+ this.__fakeObjectType = 'JdbcClob';
7
+ this._data = String(data || '');
8
+ this._isClosed = false;
9
+ }
10
+
11
+ getAsciiStream() {
12
+ if (this._isClosed) throw new Error('Clob is closed.');
13
+ return newFakeInputStream(Buffer.from(this._data, 'ascii'));
14
+ }
15
+
16
+ getCharacterStream() {
17
+ if (this._isClosed) throw new Error('Clob is closed.');
18
+ return newFakeReader(this._data);
19
+ }
20
+
21
+ getSubString(pos, len) {
22
+ if (this._isClosed) throw new Error('Clob is closed.');
23
+ // pos is 1-indexed
24
+ return this._data.substring(pos - 1, pos - 1 + len);
25
+ }
26
+
27
+ length() {
28
+ if (this._isClosed) throw new Error('Clob is closed.');
29
+ return this._data.length;
30
+ }
31
+
32
+ position(pattern, start) {
33
+ if (this._isClosed) throw new Error('Clob is closed.');
34
+ const searchPattern = (pattern instanceof FakeJdbcClob) ? pattern._data : String(pattern);
35
+ const index = this._data.indexOf(searchPattern, start - 1);
36
+ return index === -1 ? -1 : index + 1;
37
+ }
38
+
39
+ setAsciiStream(pos) {
40
+ return this.getAsciiStream();
41
+ }
42
+
43
+ setCharacterStream(pos) {
44
+ return this.getCharacterStream();
45
+ }
46
+
47
+ setString(pos, str) {
48
+ if (this._isClosed) throw new Error('Clob is closed.');
49
+ const start = pos - 1;
50
+ const prefix = this._data.substring(0, start).padEnd(start, ' ');
51
+ const suffix = this._data.substring(start + str.length);
52
+ this._data = prefix + str + suffix;
53
+ return str.length;
54
+ }
55
+
56
+ truncate(len) {
57
+ if (this._isClosed) throw new Error('Clob is closed.');
58
+ this._data = this._data.substring(0, len);
59
+ }
60
+
61
+ free() {
62
+ this._isClosed = true;
63
+ this._data = null;
64
+ }
65
+ }
66
+
67
+ export const newFakeJdbcClob = (...args) => Proxies.guard(new FakeJdbcClob(...args));
@@ -0,0 +1,66 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { Syncit } from '../../support/syncit.js';
3
+ import { newFakeJdbcStatement } from './fakejdbcstatement.js';
4
+ import { newFakeJdbcPreparedStatement } from './fakejdbcpreparedstatement.js';
5
+ import { newFakeJdbcDatabaseMetaData } from './fakejdbcdatabasemetadata.js';
6
+
7
+ class FakeJdbcConnection {
8
+ constructor(url, user, password) {
9
+ this.__fakeObjectType = 'JdbcConnection';
10
+ this._url = url;
11
+
12
+ // Connect synchronously using the worker
13
+ // Only pass arguments that are provided to avoid passing null/undefined to Syncit
14
+ const args = [url];
15
+ if (user !== null && typeof user !== 'undefined') args.push(user);
16
+ if (password !== null && typeof password !== 'undefined') args.push(password);
17
+
18
+ const result = Syncit.fxJdbcConnect(...args);
19
+ this._connectionId = result.id;
20
+ }
21
+
22
+ // GAS overload: createStatement() or createStatement(resultSetType, resultSetConcurrency)
23
+ // resultSetType/Concurrency parameters are advisory in the fake since results are buffered in memory.
24
+ createStatement(resultSetType, resultSetConcurrency) {
25
+ return newFakeJdbcStatement(this, this._connectionId);
26
+ }
27
+
28
+ prepareStatement(sql) {
29
+ return newFakeJdbcPreparedStatement(this, this._connectionId, sql);
30
+ }
31
+
32
+ commit() {
33
+ Syncit.fxJdbcCommit(this._connectionId);
34
+ }
35
+
36
+ rollback() {
37
+ Syncit.fxJdbcRollback(this._connectionId);
38
+ }
39
+
40
+ setAutoCommit(autoCommit) {
41
+ Syncit.fxJdbcSetAutoCommit(this._connectionId, autoCommit);
42
+ }
43
+
44
+ getAutoCommit() {
45
+ // We don't currently track this in the proxy, but GAS default is true
46
+ return true;
47
+ }
48
+
49
+ getMetaData() {
50
+ return newFakeJdbcDatabaseMetaData(this, this._connectionId, this._url);
51
+ }
52
+
53
+ // To match GAS JdbcConnection basic capabilities
54
+ close() {
55
+ if (this._connectionId) {
56
+ Syncit.fxJdbcClose(this._connectionId);
57
+ this._connectionId = null;
58
+ }
59
+ }
60
+
61
+ isClosed() {
62
+ return !this._connectionId;
63
+ }
64
+ }
65
+
66
+ export const newFakeJdbcConnection = (...args) => Proxies.guard(new FakeJdbcConnection(...args));