@mcpher/gas-fakes 1.2.9 → 1.2.11
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.RU.md +28 -0
- package/README.md +9 -2
- package/exgcp.sh +47 -0
- package/gas-fakes.js +468 -267
- package/package.json +5 -2
- package/src/index.js +1 -0
- package/src/services/lock/app.js +11 -0
- package/src/services/lock/fakelock.js +65 -0
- package/src/services/lock/fakelockservice.js +37 -0
- package/src/services/spreadsheetapp/fakespreadsheet.js +10 -1
package/README.RU.md
CHANGED
|
@@ -331,3 +331,31 @@ const getParentsIterator = ({
|
|
|
331
331
|
- [this file](README.md)
|
|
332
332
|
- [named colors](named-colors.md)
|
|
333
333
|
- [sandbox](sandbox.md)
|
|
334
|
+
|
|
335
|
+
## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
|
|
336
|
+
|
|
337
|
+
- [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
|
|
338
|
+
- [readme](README.md)
|
|
339
|
+
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
340
|
+
- [Inside the volatile world of a Google Document](https://ramblings.mcpher.com/inside-the-volatile-world-of-a-google-document/)
|
|
341
|
+
- [Apps Script Services on Node – using apps script libraries](https://ramblings.mcpher.com/apps-script-services-on-node-using-apps-script-libraries/)
|
|
342
|
+
- [Apps Script environment on Node – more services](https://ramblings.mcpher.com/apps-script-environment-on-node-more-services/)
|
|
343
|
+
- [Turning async into synch on Node using workers](https://ramblings.mcpher.com/turning-async-into-synch-on-node-using-workers/)
|
|
344
|
+
- [All about Apps Script Enums and how to fake them](https://ramblings.mcpher.com/all-about-apps-script-enums-and-how-to-fake-them/)
|
|
345
|
+
- [Russian version](README.RU.md) ([credit Alex Ivanov](https://github.com/oshliaer)) - needs updating
|
|
346
|
+
- [colaborators](collaborators.md) - additional information for collaborators
|
|
347
|
+
- [oddities](oddities.md) - a collection of oddities uncovered during this project
|
|
348
|
+
- [gemini](gemini-observations.md) - some reflections and experiences on using gemini to help code large projects
|
|
349
|
+
- [named colors](named-colors.md)
|
|
350
|
+
- [sandbox](sandbox.md)
|
|
351
|
+
- [named range identity](named-range-identity.md)
|
|
352
|
+
- [adc and restricted scopes](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
|
|
353
|
+
- [push test pull](pull-test-push.md)
|
|
354
|
+
- [gas fakes cli](gas-fakes-cli.md)
|
|
355
|
+
- [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
|
|
356
|
+
- [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
|
|
357
|
+
- [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
|
|
358
|
+
- [How to allow access to sensitive scopes with Application Default Credentials](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
|
|
359
|
+
- [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
|
|
360
|
+
- [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
|
|
361
|
+
- [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
|
package/README.md
CHANGED
|
@@ -150,6 +150,7 @@ For inspiration on pushing modified files to the IDE, see the togas.sh bash scri
|
|
|
150
150
|
## Help
|
|
151
151
|
|
|
152
152
|
As I mentioned earlier, to take this further, I'm going to need a lot of help to extend the methods and services supported - so if you feel this would be useful to you, and would like to collaborate, please ping me on bruce@mcpher.com and we'll talk.
|
|
153
|
+
|
|
153
154
|
## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
|
|
154
155
|
|
|
155
156
|
- [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
|
|
@@ -163,11 +164,17 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
163
164
|
- [Russian version](README.RU.md) ([credit Alex Ivanov](https://github.com/oshliaer)) - needs updating
|
|
164
165
|
- [colaborators](collaborators.md) - additional information for collaborators
|
|
165
166
|
- [oddities](oddities.md) - a collection of oddities uncovered during this project
|
|
166
|
-
- [gemini](gemini.md) - some reflections and experiences on using gemini to help code large projects
|
|
167
|
+
- [gemini](gemini-observations.md) - some reflections and experiences on using gemini to help code large projects
|
|
167
168
|
- [named colors](named-colors.md)
|
|
168
169
|
- [sandbox](sandbox.md)
|
|
169
170
|
- [named range identity](named-range-identity.md)
|
|
170
171
|
- [adc and restricted scopes](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
|
|
171
172
|
- [push test pull](pull-test-push.md)
|
|
172
173
|
- [gas fakes cli](gas-fakes-cli.md)
|
|
173
|
-
- [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
|
|
174
|
+
- [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
|
|
175
|
+
- [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
|
|
176
|
+
- [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
|
|
177
|
+
- [How to allow access to sensitive scopes with Application Default Credentials](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
|
|
178
|
+
- [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
|
|
179
|
+
- [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
|
|
180
|
+
- [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
|
package/exgcp.sh
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# This script reads the GCP_PROJECT_ID from a .env file
|
|
4
|
+
# and exports it as GOOGLE_CLOUD_PROJECT for the current shell session.
|
|
5
|
+
#
|
|
6
|
+
# Usage: source . ./exgcp.sh
|
|
7
|
+
|
|
8
|
+
# Define the path to your .env file relative to the script's location
|
|
9
|
+
ENV_FILE="$(dirname "$0")/.env"
|
|
10
|
+
|
|
11
|
+
# Check if the .env file exists
|
|
12
|
+
|
|
13
|
+
if [ ! -f "$ENV_FILE" ]; then
|
|
14
|
+
echo "Error: .env file not found at path: $ENV_FILE"
|
|
15
|
+
# Use 'return' instead of 'exit' so it doesn't close the user's terminal when sourced
|
|
16
|
+
return 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Read the GCP_PROJECT_ID, remove quotes, and handle potential carriage returns
|
|
20
|
+
GCP_PROJECT_ID_VALUE=$(grep -E '^GCP_PROJECT_ID=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
21
|
+
GEMINI_API_KEY_VALUE=$(grep -E '^GEMINI_API_KEY=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
22
|
+
GEMINI_MODEL_VALUE=$(grep -E '^GEMINI_MODEL=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
23
|
+
|
|
24
|
+
# Check if a value was extracted
|
|
25
|
+
if [ -z "$GCP_PROJECT_ID_VALUE" ]; then
|
|
26
|
+
echo "Error: GCP_PROJECT_ID not found or is empty in $ENV_FILE."
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [ -z "GEMINI_API_KEY_VALUE" ]; then
|
|
31
|
+
echo "GEMINI_API_KEY not found or is empty in $ENV_FILE."
|
|
32
|
+
else
|
|
33
|
+
echo "exported: GEMINI_API_KEY"
|
|
34
|
+
export GEMINI_API_KEY="$GEMINI_API_KEY_VALUE"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ -z "GEMINI_MODEL_VALUE" ]; then
|
|
38
|
+
echo "GEMINI_MODEL not found or is empty in $ENV_FILE."
|
|
39
|
+
else
|
|
40
|
+
echo "exported: GEMINI_MODEL=$GEMINI_MODEL_VALUE"
|
|
41
|
+
export GEMINI_MODEL="$GEMINI_MODEL_VALUE"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Export the variable for the current session
|
|
45
|
+
export GOOGLE_CLOUD_PROJECT="$GCP_PROJECT_ID_VALUE"
|
|
46
|
+
|
|
47
|
+
echo "exported: GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT"
|
package/gas-fakes.js
CHANGED
|
@@ -1,309 +1,510 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
// IMPORTS
|
|
5
|
+
// -----------------------------------------------------------------------------
|
|
6
|
+
|
|
6
7
|
import fs from "fs";
|
|
7
8
|
import path from "path";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
8
11
|
import { Command } from "commander";
|
|
9
12
|
import dotenv from "dotenv";
|
|
10
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
-
import z from "zod";
|
|
13
|
-
import { exec } from "child_process";
|
|
14
|
-
import { promisify } from "util";
|
|
15
|
+
import { z, ZodCatch } from "zod";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.name("gas-fakes")
|
|
22
|
-
.description("CLI tool for gas-fakes")
|
|
23
|
-
.version(version, "-v, --version", "display the current version");
|
|
24
|
-
|
|
25
|
-
program
|
|
26
|
-
.description("Execute Google Apps Script using gas-fakes.")
|
|
27
|
-
.option(
|
|
28
|
-
"-f, --filename <string>",
|
|
29
|
-
"filename of the file including Google Apps Script. When this is used, the option --script is ignored."
|
|
30
|
-
)
|
|
31
|
-
.option(
|
|
32
|
-
"-e, --env <path>",
|
|
33
|
-
"provide path to your .env file for special options.",
|
|
34
|
-
"./.env"
|
|
35
|
-
)
|
|
36
|
-
.option(
|
|
37
|
-
"-g, --gfsettings <path>",
|
|
38
|
-
"provide path to your gasfakes.json file for script options.",
|
|
39
|
-
"./gasfakes.json"
|
|
40
|
-
)
|
|
41
|
-
.option(
|
|
42
|
-
"-s, --script <string>",
|
|
43
|
-
"provide Google Apps Script as a string. When this is used, the option --filename is ignored."
|
|
44
|
-
)
|
|
45
|
-
.option("-x, --sandbox", "run Google Apps Script in a sandbox.")
|
|
46
|
-
.option(
|
|
47
|
-
"-w, --whitelist <string>",
|
|
48
|
-
"whitelist of file IDs. Set the file IDs in comma-separated list. In this case, the files of the file IDs are used for both read and write. When this is used, the script is run in a sandbox."
|
|
49
|
-
)
|
|
50
|
-
.option(
|
|
51
|
-
"-j, --json <string>",
|
|
52
|
-
`JSON string including parameters for managing a sandbox. Enclose it with ' or ". When this is used, the option --whitelist is ignored. When this is used, the script is run in a sandbox.`
|
|
53
|
-
)
|
|
54
|
-
.option(
|
|
55
|
-
"-d, --display",
|
|
56
|
-
`display the created script for executing with gas-fakes. Default is false.`,
|
|
57
|
-
false
|
|
58
|
-
)
|
|
59
|
-
.action((options) => {
|
|
60
|
-
if (Object.keys(options).length == 0) {
|
|
61
|
-
program.help();
|
|
62
|
-
} else {
|
|
63
|
-
const {
|
|
64
|
-
filename,
|
|
65
|
-
script,
|
|
66
|
-
sandbox,
|
|
67
|
-
whitelist,
|
|
68
|
-
json,
|
|
69
|
-
display,
|
|
70
|
-
env,
|
|
71
|
-
gfsettings,
|
|
72
|
-
} = options;
|
|
73
|
-
const obj = { sandbox: !!sandbox, display };
|
|
74
|
-
|
|
75
|
-
if (!filename && !script && !obj.script) {
|
|
76
|
-
console.error(
|
|
77
|
-
"error: Provide the filename or the script of Google Apps Script."
|
|
78
|
-
);
|
|
79
|
-
process.exit();
|
|
80
|
-
}
|
|
17
|
+
// -----------------------------------------------------------------------------
|
|
18
|
+
// CONSTANTS & UTILITIES
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
81
20
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
dotenv.config({ path: envPath, quiet: true });
|
|
86
|
-
}
|
|
21
|
+
const VERSION = "0.0.6";
|
|
22
|
+
const MCP_VERSION = "0.0.3";
|
|
23
|
+
const execAsync = promisify(exec);
|
|
87
24
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Replaces escaped newline characters ('\\n') with actual newlines,
|
|
27
|
+
* while ignoring newlines inside string literals.
|
|
28
|
+
* @param {string} text The script text to process.
|
|
29
|
+
* @returns {string} The processed text.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeScriptNewlines(text) {
|
|
32
|
+
const regex = /("[^"]*")|('[^']*')|(`[^`]*`)|(\\n)/g;
|
|
33
|
+
return text.replace(regex, (match, g1, g2, g3, g4) => (g4 ? "\n" : match));
|
|
34
|
+
}
|
|
96
35
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!obj.script && script) {
|
|
101
|
-
obj.script = script;
|
|
102
|
-
}
|
|
36
|
+
// -----------------------------------------------------------------------------
|
|
37
|
+
// SANDBOX SCRIPT GENERATION
|
|
38
|
+
// -----------------------------------------------------------------------------
|
|
103
39
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Generates the GAS script snippet for whitelisting services.
|
|
42
|
+
* @param {Array<Object>} services The whitelistServices configuration.
|
|
43
|
+
* @returns {string[]} An array of script lines.
|
|
44
|
+
*/
|
|
45
|
+
function generateServiceWhitelistScript(services) {
|
|
46
|
+
if (!services || services.length === 0) return [];
|
|
47
|
+
|
|
48
|
+
return services.flatMap(({ className, methodNames }, index) => {
|
|
49
|
+
if (!className) {
|
|
50
|
+
console.error("Error: Class name not found in whitelistServices.");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const serviceVar = `service${index + 1}`;
|
|
54
|
+
const lines = [
|
|
55
|
+
`const ${serviceVar} = behavior.sandboxService.${className};`,
|
|
56
|
+
];
|
|
57
|
+
if (methodNames && methodNames.length > 0) {
|
|
58
|
+
const methods = methodNames.map((name) => `"${name}"`).join(", ");
|
|
59
|
+
lines.push(`${serviceVar}.setMethodWhitelist([${methods}]);`);
|
|
121
60
|
}
|
|
61
|
+
return lines;
|
|
122
62
|
});
|
|
63
|
+
}
|
|
123
64
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Generates the GAS script snippet for blacklisting services.
|
|
67
|
+
* @param {string[]} services The blacklistServices configuration.
|
|
68
|
+
* @returns {string[]} An array of script lines.
|
|
69
|
+
*/
|
|
70
|
+
function generateServiceBlacklistScript(services) {
|
|
71
|
+
if (!services || services.length === 0) return [];
|
|
72
|
+
return services.map(
|
|
73
|
+
(service) => `behavior.sandboxService.${service}.enabled = false;`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generates the GAS script snippet for whitelisting Drive items.
|
|
79
|
+
* @param {Array<Object>} items The whitelistItems configuration.
|
|
80
|
+
* @returns {string} A script string.
|
|
81
|
+
*/
|
|
82
|
+
function generateItemWhitelistScript(items) {
|
|
83
|
+
if (!items || items.length === 0) return "";
|
|
84
|
+
|
|
85
|
+
const whitelistItemsString = items
|
|
86
|
+
.map(({ itemId = "", read = true, write = false, trash = false }) => {
|
|
87
|
+
if (!itemId) {
|
|
88
|
+
console.error("Error: itemId not found in whitelistItems.");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return `behavior.newIdWhitelistItem("${itemId}").setRead(${read}).setWrite(${write}).setTrash(${trash})`;
|
|
92
|
+
})
|
|
93
|
+
.join(",\n ");
|
|
94
|
+
|
|
95
|
+
return `behavior.setIdWhitelist([${whitelistItemsString}]);`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Constructs the setup script for a sandboxed environment based on configuration.
|
|
100
|
+
* @param {object} sandboxConfig The sandbox configuration object.
|
|
101
|
+
* @returns {string[]} An array of GAS script lines for setup.
|
|
102
|
+
*/
|
|
103
|
+
function generateSandboxSetupScript(sandboxConfig) {
|
|
104
|
+
const script = [
|
|
105
|
+
"const behavior = ScriptApp.__behavior;",
|
|
106
|
+
"behavior.sandboxMode = true;",
|
|
107
|
+
"behavior.strictSandbox = true;",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const { whitelistServices, blacklistServices, whitelistItems } =
|
|
111
|
+
sandboxConfig;
|
|
112
|
+
|
|
113
|
+
script.push(...generateServiceWhitelistScript(whitelistServices));
|
|
114
|
+
script.push(...generateServiceBlacklistScript(blacklistServices));
|
|
115
|
+
|
|
116
|
+
const itemWhitelist = generateItemWhitelistScript(whitelistItems);
|
|
117
|
+
if (itemWhitelist) {
|
|
118
|
+
script.push(itemWhitelist);
|
|
138
119
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
if (blacklistServices && blacklistServices.length > 0) {
|
|
169
|
-
const bl = blacklistServices.map(
|
|
170
|
-
(e) => `behavior.sandboxService.${e}.enabled = false;`
|
|
171
|
-
);
|
|
172
|
-
gasScriptAr.push(bl);
|
|
173
|
-
}
|
|
174
|
-
if (whitelistItems && whitelistItems.length > 0) {
|
|
175
|
-
const wl = whitelistItems
|
|
176
|
-
.map(({ itemId = "", read = true, write = false, trash = false }) => {
|
|
177
|
-
if (!itemId) {
|
|
178
|
-
console.error("error: itemId was not found in whitelistItems.");
|
|
179
|
-
process.exit();
|
|
180
|
-
}
|
|
181
|
-
return `behavior.newIdWhitelistItem("${itemId}").setRead(${read}).setWrite(${write}).setTrash(${trash})`;
|
|
182
|
-
})
|
|
183
|
-
.join(",");
|
|
184
|
-
gasScriptAr.push(`behavior.setIdWhitelist([${wl}]);`);
|
|
185
|
-
}
|
|
186
|
-
gasScriptAr.push(`\n\n${scriptText}\n\n`, `ScriptApp.__behavior.trash();`);
|
|
120
|
+
|
|
121
|
+
return script;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generates the final, executable script string.
|
|
126
|
+
* @param {object} options
|
|
127
|
+
* @param {string} options.scriptText The user's Google Apps Script.
|
|
128
|
+
* @param {boolean} options.useSandbox Whether to enable basic sandbox mode.
|
|
129
|
+
* @param {object} [options.sandboxConfig] Detailed sandbox configuration.
|
|
130
|
+
* @returns {{mainScript: string, gasScript: string}} The script to be executed and the user-facing script part.
|
|
131
|
+
*/
|
|
132
|
+
function generateExecutionScript({ scriptText, useSandbox, sandboxConfig }) {
|
|
133
|
+
if (!scriptText || scriptText.trim() === "") {
|
|
134
|
+
console.error("Error: Google Apps Script is empty or was not found.");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let gasScriptLines = [];
|
|
139
|
+
|
|
140
|
+
if (sandboxConfig) {
|
|
141
|
+
gasScriptLines.push(...generateSandboxSetupScript(sandboxConfig));
|
|
142
|
+
gasScriptLines.push(`\n\n${scriptText}\n\n`);
|
|
143
|
+
gasScriptLines.push("ScriptApp.__behavior.trash();");
|
|
144
|
+
} else if (useSandbox) {
|
|
145
|
+
gasScriptLines.push("ScriptApp.__behavior.sandBoxMode = true;");
|
|
146
|
+
gasScriptLines.push(`\n\n${scriptText}\n\n`);
|
|
147
|
+
gasScriptLines.push("ScriptApp.__behavior.trash();");
|
|
187
148
|
} else {
|
|
188
|
-
|
|
189
|
-
gasScriptAr.push(
|
|
190
|
-
sandbox ? `ScriptApp.__behavior.sandBoxMode = true;` : "",
|
|
191
|
-
`\n\n${scriptText}\n\n`,
|
|
192
|
-
sandbox ? `ScriptApp.__behavior.trash();` : ""
|
|
193
|
-
);
|
|
194
|
-
} else if (whitelistItems && whitelistItems.length > 0) {
|
|
195
|
-
const wl = whitelistItems
|
|
196
|
-
.map((id) => `behavior.newIdWhitelistItem("${id}").setWrite(true)`)
|
|
197
|
-
.join(",");
|
|
198
|
-
gasScriptAr.push(
|
|
199
|
-
`const behavior = ScriptApp.__behavior;`,
|
|
200
|
-
`behavior.sandboxMode = true;`,
|
|
201
|
-
`behavior.strictSandbox = true;`,
|
|
202
|
-
`behavior.setIdWhitelist([${wl}]);``\n\n${scriptText}\n\n`,
|
|
203
|
-
`ScriptApp.__behavior.trash();`
|
|
204
|
-
);
|
|
205
|
-
} else {
|
|
206
|
-
gasScriptAr.push(scriptText);
|
|
207
|
-
}
|
|
149
|
+
gasScriptLines.push(scriptText);
|
|
208
150
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
};
|
|
151
|
+
|
|
152
|
+
const gasScript = gasScriptLines.join("\n");
|
|
153
|
+
const mainScript = [
|
|
154
|
+
"async function runGas() {",
|
|
155
|
+
' await import("./main.js"); // This will trigger the fxInit call',
|
|
156
|
+
gasScript,
|
|
157
|
+
"}",
|
|
158
|
+
"runGas();",
|
|
159
|
+
].join("\n");
|
|
160
|
+
|
|
161
|
+
return { mainScript, gasScript };
|
|
221
162
|
}
|
|
222
163
|
|
|
223
|
-
|
|
224
|
-
|
|
164
|
+
// -----------------------------------------------------------------------------
|
|
165
|
+
// SCRIPT EXECUTION
|
|
166
|
+
// -----------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Loads, prepares, and executes the user's Google Apps Script.
|
|
170
|
+
* @param {object} options The processed CLI options.
|
|
171
|
+
*/
|
|
172
|
+
async function executeGasScript(options) {
|
|
173
|
+
const {
|
|
174
|
+
filename,
|
|
175
|
+
script,
|
|
176
|
+
display,
|
|
177
|
+
gfSettings,
|
|
178
|
+
useSandbox,
|
|
179
|
+
sandboxConfig,
|
|
180
|
+
args,
|
|
181
|
+
} = options;
|
|
182
|
+
|
|
225
183
|
const scriptText = filename ? fs.readFileSync(filename, "utf8") : script;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
184
|
+
|
|
185
|
+
const { mainScript, gasScript } = generateExecutionScript({
|
|
186
|
+
scriptText: normalizeScriptNewlines(scriptText),
|
|
187
|
+
useSandbox,
|
|
188
|
+
sandboxConfig,
|
|
229
189
|
});
|
|
190
|
+
|
|
230
191
|
if (display) {
|
|
231
|
-
console.log(
|
|
192
|
+
console.log(
|
|
193
|
+
`\n--- Generated GAS ---\n${gasScript}\n--- End Generated GAS ---\n`
|
|
194
|
+
);
|
|
232
195
|
}
|
|
233
|
-
|
|
234
|
-
//
|
|
196
|
+
|
|
197
|
+
// Inject the settings path as a global for the script to access.
|
|
235
198
|
Object.defineProperty(globalThis, "settingsPath", {
|
|
236
|
-
value:
|
|
199
|
+
value: gfSettings,
|
|
237
200
|
writable: true,
|
|
238
201
|
configurable: true,
|
|
239
202
|
});
|
|
240
|
-
|
|
203
|
+
|
|
204
|
+
if (args) {
|
|
205
|
+
const gasFunction = new Function("args", mainScript);
|
|
206
|
+
await gasFunction(args);
|
|
207
|
+
} else {
|
|
208
|
+
const gasFunction = new Function(mainScript);
|
|
209
|
+
await gasFunction();
|
|
210
|
+
}
|
|
241
211
|
}
|
|
242
212
|
|
|
243
|
-
|
|
213
|
+
// -----------------------------------------------------------------------------
|
|
214
|
+
// MCP SERVER
|
|
215
|
+
// -----------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Defines and runs the MCP server for gas-fakes.
|
|
219
|
+
*/
|
|
220
|
+
async function startMcpServer() {
|
|
244
221
|
const server = new McpServer({
|
|
245
222
|
name: "gas-fakes-mcp",
|
|
246
|
-
version:
|
|
223
|
+
version: MCP_VERSION,
|
|
247
224
|
});
|
|
248
225
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
226
|
+
const mcpToolSchema = {
|
|
227
|
+
description: [
|
|
228
|
+
`Use this to safely run Google Apps Script in a sandbox using gas-fakes.`,
|
|
229
|
+
`# Important`,
|
|
230
|
+
`- Use the extension of the Google Apps Script files as \`js\`. Don't use \`gs\``,
|
|
231
|
+
`- When you provide the generated Google Apps Script to the tool "gas-fakes" of the MCP server "gas-development-kit-extension", please be careful of the following rule. For example, when you generated a Google Apps Script like \`function sample() { script }\`, please add \`sample();\` to execute the function. Or, you can also create a Google Apps Script without enclosing the script with \`function sample() { script }\`.`,
|
|
232
|
+
].join("\n"),
|
|
233
|
+
inputSchema: {
|
|
234
|
+
filename: z
|
|
235
|
+
.string()
|
|
236
|
+
.describe(
|
|
237
|
+
`Provide a filename with the path of the file, including Google Apps Script. Write the Google Apps Script into a file and use this.`
|
|
238
|
+
),
|
|
239
|
+
sandbox: z
|
|
240
|
+
.boolean()
|
|
241
|
+
.describe("Use to run Google Apps Script in a sandbox."),
|
|
242
|
+
whitelistRead: z
|
|
243
|
+
.string()
|
|
244
|
+
.describe(
|
|
245
|
+
"Whitelist of file IDs for readonly access (comma-separated). Enables sandbox mode."
|
|
246
|
+
)
|
|
247
|
+
.optional(),
|
|
248
|
+
whitelistReadWrite: z
|
|
249
|
+
.string()
|
|
250
|
+
.describe(
|
|
251
|
+
"Whitelist of file IDs for read/write access (comma-separated). Enables sandbox mode."
|
|
252
|
+
)
|
|
253
|
+
.optional(),
|
|
254
|
+
whitelistReadWriteTrash: z
|
|
255
|
+
.string()
|
|
256
|
+
.describe(
|
|
257
|
+
"Whitelist of file IDs for read/write/trash access (comma-separated). Enables sandbox mode."
|
|
258
|
+
)
|
|
259
|
+
.optional(),
|
|
260
|
+
json: z
|
|
261
|
+
.object({
|
|
262
|
+
whitelistItems: z
|
|
263
|
+
.array(
|
|
264
|
+
z.object({
|
|
265
|
+
itemId: z
|
|
266
|
+
.string()
|
|
267
|
+
.describe("The file or folder ID on Google Drive."),
|
|
268
|
+
read: z.boolean().optional().default(true),
|
|
269
|
+
write: z.boolean().optional().default(false),
|
|
270
|
+
trash: z.boolean().optional().default(false),
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
.describe("A list of items to be whitelisted."),
|
|
274
|
+
whitelistServices: z
|
|
275
|
+
.array(
|
|
276
|
+
z.object({
|
|
277
|
+
className: z
|
|
278
|
+
.string()
|
|
279
|
+
.describe("The class name of the GAS service."),
|
|
280
|
+
methodNames: z
|
|
281
|
+
.array(z.string())
|
|
282
|
+
.describe(
|
|
283
|
+
"A list of method names for the class to be whitelisted."
|
|
284
|
+
)
|
|
285
|
+
.optional(),
|
|
286
|
+
})
|
|
287
|
+
)
|
|
288
|
+
.describe("A list of services to be whitelisted.")
|
|
289
|
+
.optional(),
|
|
290
|
+
blacklistServices: z
|
|
291
|
+
.array(z.string())
|
|
292
|
+
.describe("A list of GAS services to be blacklisted.")
|
|
293
|
+
.optional(),
|
|
294
|
+
})
|
|
295
|
+
.describe("A JSON object for advanced sandbox configuration.")
|
|
296
|
+
.optional(),
|
|
302
297
|
},
|
|
303
298
|
};
|
|
304
299
|
|
|
305
|
-
|
|
300
|
+
const mcpToolFunc = async (options = {}) => {
|
|
301
|
+
const {
|
|
302
|
+
filename,
|
|
303
|
+
sandbox,
|
|
304
|
+
whitelistRead,
|
|
305
|
+
whitelistReadWrite,
|
|
306
|
+
whitelistReadWriteTrash,
|
|
307
|
+
json,
|
|
308
|
+
} = options;
|
|
309
|
+
|
|
310
|
+
if (!filename) {
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{ type: "text", text: "Error: `filename` is a required parameter." },
|
|
314
|
+
],
|
|
315
|
+
isError: true,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const cliArgs = [];
|
|
321
|
+
cliArgs.push(`-f "${filename}"`);
|
|
322
|
+
if (sandbox) cliArgs.push("-x");
|
|
323
|
+
if (whitelistRead) cliArgs.push(`-w "${whitelistRead}"`);
|
|
324
|
+
if (whitelistReadWrite) cliArgs.push(`--ww "${whitelistReadWrite}"`);
|
|
325
|
+
if (whitelistReadWriteTrash)
|
|
326
|
+
cliArgs.push(`--wt "${whitelistReadWriteTrash}"`);
|
|
327
|
+
if (json) cliArgs.push(`-j '${JSON.stringify(json)}'`);
|
|
328
|
+
|
|
329
|
+
const command = `gas-fakes ${cliArgs.join(" ")}`;
|
|
330
|
+
const { stdout } = await execAsync(command);
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: stdout || "Execution finished." }],
|
|
333
|
+
isError: false,
|
|
334
|
+
};
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: "text", text: err.message }],
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
server.registerTool("run-gas-by-gas-fakes", mcpToolSchema, mcpToolFunc);
|
|
306
344
|
|
|
307
345
|
const transport = new StdioServerTransport();
|
|
308
346
|
await server.connect(transport);
|
|
309
347
|
}
|
|
348
|
+
|
|
349
|
+
// -----------------------------------------------------------------------------
|
|
350
|
+
// CLI DEFINITION & MAIN EXECUTION
|
|
351
|
+
// -----------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parses sandbox-related CLI options into a structured config object.
|
|
355
|
+
* @param {object} options Raw options from Commander.
|
|
356
|
+
* @returns {object | undefined} A sandbox configuration object or undefined.
|
|
357
|
+
*/
|
|
358
|
+
function buildSandboxConfig(options) {
|
|
359
|
+
const { json, whitelistRead, whitelistReadWrite, whitelistReadWriteTrash } =
|
|
360
|
+
options;
|
|
361
|
+
|
|
362
|
+
if (json) {
|
|
363
|
+
try {
|
|
364
|
+
return JSON.parse(json);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error("Error: Invalid JSON provided to --json option.");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (whitelistRead || whitelistReadWrite || whitelistReadWriteTrash) {
|
|
372
|
+
const config = { whitelistItems: [] };
|
|
373
|
+
const parseWhitelist = (idString, permissions) => {
|
|
374
|
+
if (!idString) return;
|
|
375
|
+
idString.split(",").forEach((id) => {
|
|
376
|
+
const trimmedId = id.trim();
|
|
377
|
+
if (trimmedId) {
|
|
378
|
+
config.whitelistItems.push({ itemId: trimmedId, ...permissions });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
parseWhitelist(whitelistRead, { read: true });
|
|
384
|
+
parseWhitelist(whitelistReadWrite, { read: true, write: true });
|
|
385
|
+
parseWhitelist(whitelistReadWriteTrash, {
|
|
386
|
+
read: true,
|
|
387
|
+
write: true,
|
|
388
|
+
trash: true,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return config;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Sets up and runs the command-line interface.
|
|
399
|
+
*/
|
|
400
|
+
async function main() {
|
|
401
|
+
const program = new Command();
|
|
402
|
+
|
|
403
|
+
program
|
|
404
|
+
.name("gas-fakes")
|
|
405
|
+
.description("A CLI tool to execute Google Apps Script with fakes/mocks.")
|
|
406
|
+
.version(VERSION, "-v, --version", "Display the current version");
|
|
407
|
+
|
|
408
|
+
program
|
|
409
|
+
.description("Execute a Google Apps Script file or string.")
|
|
410
|
+
.option("-f, --filename <string>", "Path to the Google Apps Script file.")
|
|
411
|
+
.option(
|
|
412
|
+
"-s, --script <string>",
|
|
413
|
+
"A string containing the Google Apps Script."
|
|
414
|
+
)
|
|
415
|
+
.option("-e, --env <path>", "Path to a custom .env file.", "./.env")
|
|
416
|
+
.option(
|
|
417
|
+
"-g, --gfsettings <path>",
|
|
418
|
+
"Path to a gasfakes.json settings file.",
|
|
419
|
+
"./gasfakes.json"
|
|
420
|
+
)
|
|
421
|
+
.option("-x, --sandbox", "Run the script in a basic sandbox.")
|
|
422
|
+
.option(
|
|
423
|
+
"-w, --whitelistRead <string>",
|
|
424
|
+
"Comma-separated file IDs for read-only access (enables sandbox)."
|
|
425
|
+
)
|
|
426
|
+
.option(
|
|
427
|
+
"--ww, --whitelistReadWrite <string>",
|
|
428
|
+
"Comma-separated file IDs for read/write access (enables sandbox)."
|
|
429
|
+
)
|
|
430
|
+
.option(
|
|
431
|
+
"--wt, --whitelistReadWriteTrash <string>",
|
|
432
|
+
"Comma-separated file IDs for read/write/trash access (enables sandbox)."
|
|
433
|
+
)
|
|
434
|
+
.option(
|
|
435
|
+
"-j, --json <string>",
|
|
436
|
+
"JSON string for advanced sandbox configuration (overrides whitelist flags)."
|
|
437
|
+
)
|
|
438
|
+
.option(
|
|
439
|
+
"-d, --display",
|
|
440
|
+
"Display the generated script before execution.",
|
|
441
|
+
false
|
|
442
|
+
)
|
|
443
|
+
.option(
|
|
444
|
+
"-a, --args <string>",
|
|
445
|
+
`Arguments for the function of Google Apps Script. Provide it as a JSON string. The name of the argument is "args" as a fixed name. For example, when the function of GAS is \`function sample(args) { script }\`, you can provide the arguments like \`-a '{"key": "value"}'\`.`,
|
|
446
|
+
null
|
|
447
|
+
)
|
|
448
|
+
.action(async (options) => {
|
|
449
|
+
if (Object.keys(options).length === 0) {
|
|
450
|
+
program.help();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const { filename, script, env, gfsettings } = options;
|
|
455
|
+
if (!filename && !script) {
|
|
456
|
+
console.error(
|
|
457
|
+
"Error: You must provide a script via --filename or --script."
|
|
458
|
+
);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Load environment variables
|
|
463
|
+
const envPath = path.resolve(process.cwd(), env);
|
|
464
|
+
console.log(`...using env file in ${envPath}`);
|
|
465
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
466
|
+
|
|
467
|
+
// Load gasfakes settings
|
|
468
|
+
const settingsPath = path.resolve(process.cwd(), gfsettings);
|
|
469
|
+
console.log(`...using gasfakes settings file in ${settingsPath}`);
|
|
470
|
+
process.env.GF_SETTINGS_PATH = settingsPath;
|
|
471
|
+
|
|
472
|
+
const sandboxConfig = buildSandboxConfig(options);
|
|
473
|
+
const useSandbox = !!options.sandbox || !!sandboxConfig;
|
|
474
|
+
|
|
475
|
+
let args = null;
|
|
476
|
+
if (options.args) {
|
|
477
|
+
try {
|
|
478
|
+
args = JSON.parse(options.args);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error("Error: Invalid JSON provided to --args option.");
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
await executeGasScript({
|
|
486
|
+
filename,
|
|
487
|
+
script,
|
|
488
|
+
display: options.display,
|
|
489
|
+
useSandbox,
|
|
490
|
+
sandboxConfig,
|
|
491
|
+
gfSettings: settingsPath,
|
|
492
|
+
args,
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
program
|
|
497
|
+
.command("mcp")
|
|
498
|
+
.description("Launch gas-fakes as an MCP server.")
|
|
499
|
+
.action(startMcpServer);
|
|
500
|
+
|
|
501
|
+
program.showHelpAfterError("(add --help for additional information)");
|
|
502
|
+
|
|
503
|
+
await program.parseAsync(process.argv);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Run the main function
|
|
507
|
+
main().catch((err) => {
|
|
508
|
+
console.error("An unexpected error occurred:", err);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
});
|
package/package.json
CHANGED
|
@@ -30,11 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"type": "module",
|
|
32
32
|
"scripts": {
|
|
33
|
-
"pub": "npm publish --access public"
|
|
33
|
+
"pub": "npm publish --access public",
|
|
34
|
+
"includes": "node ./doccreation/include-docs.js",
|
|
35
|
+
"progress": "cd ./doccreation && bash pipeline.sh",
|
|
36
|
+
"docs": "npm run includes && npm run progress"
|
|
34
37
|
},
|
|
35
38
|
"name": "@mcpher/gas-fakes",
|
|
36
39
|
"author": "bruce mcpherson",
|
|
37
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.11",
|
|
38
41
|
"license": "MIT",
|
|
39
42
|
"main": "main.js",
|
|
40
43
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* the idea here is to create an empty global entry for the singleton
|
|
5
|
+
* but only load it when it is actually used.
|
|
6
|
+
*/
|
|
7
|
+
import { newFakeLockService as maker } from './fakelockservice.js';
|
|
8
|
+
import { lazyLoaderApp } from '../common/lazyloader.js'
|
|
9
|
+
|
|
10
|
+
let _app = null;
|
|
11
|
+
_app = lazyLoaderApp(_app, 'LockService', maker)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* the lock service us meaningless here as we are running on node single threaded
|
|
6
|
+
* in apps script this would lock shared code that was being shared by multiple scripts
|
|
7
|
+
* so this is all provided for compatibility only
|
|
8
|
+
*/
|
|
9
|
+
class FakeLock {
|
|
10
|
+
constructor(domain) {
|
|
11
|
+
this.__fakeObjectType = 'Lock';
|
|
12
|
+
this.__domain = domain
|
|
13
|
+
this.__locked = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if the lock was acquired.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
hasLock() {
|
|
21
|
+
return this.__locked;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Releases the lock.
|
|
26
|
+
*/
|
|
27
|
+
releaseLock() {
|
|
28
|
+
this.__locked = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Attempts to acquire the lock.
|
|
33
|
+
* @param {number} timeoutInMillis
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
tryLock(timeoutInMillis) {
|
|
37
|
+
if (this.hasLock()) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// In a single-threaded fake, we can't wait. We fail only if timeout is negative.
|
|
41
|
+
if (timeoutInMillis < 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
this.__locked = true;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Attempts to acquire the lock, throwing an exception on timeout.
|
|
50
|
+
* @param {number} timeoutInMillis
|
|
51
|
+
*/
|
|
52
|
+
waitLock(timeoutInMillis) {
|
|
53
|
+
if (this.hasLock()) {
|
|
54
|
+
return; // Already acquired, no need to wait
|
|
55
|
+
}
|
|
56
|
+
// In a single-threaded fake, we can't wait. We fail only if timeout is negative.
|
|
57
|
+
if (timeoutInMillis < 0) {
|
|
58
|
+
throw new Error(`Lock timeout: another process was holding the lock for too long.`);
|
|
59
|
+
}
|
|
60
|
+
this.__locked = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export const newFakeLock = (...args) => Proxies.guard(new FakeLock(...args));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeLock } from './fakelock.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* domains of lock supported
|
|
6
|
+
* @enum {string}
|
|
7
|
+
*/
|
|
8
|
+
const LockDomain = Object.freeze({
|
|
9
|
+
SCRIPT: 'SCRIPT',
|
|
10
|
+
USER: 'USER',
|
|
11
|
+
DOCUMENT: 'DOCUMENT'
|
|
12
|
+
})
|
|
13
|
+
/**
|
|
14
|
+
* the lock service us meaningless here as we are running on node single threaded
|
|
15
|
+
* in apps script this would lock shared code that was being shared by multiple scripts
|
|
16
|
+
* so this is all provided for compatibility only
|
|
17
|
+
*/
|
|
18
|
+
class FakeLockService {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.__fakeObjectType = 'LockService';
|
|
21
|
+
}
|
|
22
|
+
getDocumentLock () {
|
|
23
|
+
// Per documentation, this should return null if not in the context of a document.
|
|
24
|
+
if (ScriptApp.__documentId) {
|
|
25
|
+
return newFakeLock(LockDomain.DOCUMENT);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
getUserLock () {
|
|
30
|
+
return newFakeLock(LockDomain.USER)
|
|
31
|
+
}
|
|
32
|
+
getScriptLock () {
|
|
33
|
+
return newFakeLock(LockDomain.SCRIPT)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const newFakeLockService = (...args) => Proxies.guard(new FakeLockService(...args));
|
|
@@ -42,7 +42,7 @@ export class FakeSpreadsheet {
|
|
|
42
42
|
const props = [
|
|
43
43
|
"getSpreadsheetTheme",
|
|
44
44
|
"setActiveSheet",
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
"getBandings",
|
|
47
47
|
"getDataSources",
|
|
48
48
|
"addCollaborator",
|
|
@@ -171,8 +171,17 @@ export class FakeSpreadsheet {
|
|
|
171
171
|
return notYetImplemented(f);
|
|
172
172
|
};
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// note this is a workaround as we don't have the concept of active sheet in a non bound document
|
|
178
|
+
// so instead we'll just get the first sheet for now
|
|
179
|
+
// TODO - something better
|
|
180
|
+
getActiveSheet() {
|
|
181
|
+
return this.__getFirstSheet();
|
|
174
182
|
}
|
|
175
183
|
|
|
184
|
+
|
|
176
185
|
addDeveloperMetadata(key, value, visibility) {
|
|
177
186
|
const { nargs, matchThrow } = signatureArgs(
|
|
178
187
|
arguments,
|