@mcpher/gas-fakes 1.2.25 → 1.2.26
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 +3 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/cli/app.js +16 -1
- package/src/cli/executor.js +25 -1
- package/src/cli/mcp.js +5 -4
- package/src/cli/setup.js +356 -53
- package/src/cli/utils.js +2 -2
- package/src/index.js +1 -0
- package/src/services/advdrive/fakeadvdrivefiles.js +13 -36
- package/src/services/libhandlerapp/app.js +9 -0
- package/src/services/libhandlerapp/fakelibhandler.js +39 -0
- package/src/services/libhandlerapp/fakelibhandlerapp.js +54 -0
- package/src/services/libhandlerapp/fakelibrary.js +117 -0
- package/src/services/scriptapp/behavior.js +1 -1
- package/src/support/sxdrive.js +35 -15
- package/src/support/syncit.js +19 -2
- package/testlib.sh +2 -1
package/README.RU.md
CHANGED
|
@@ -348,6 +348,9 @@ const getParentsIterator = ({
|
|
|
348
348
|
- [gemini](gemini-observations.md) - some reflections and experiences on using gemini to help code large projects
|
|
349
349
|
- [named colors](named-colors.md)
|
|
350
350
|
- [sandbox](sandbox.md)
|
|
351
|
+
- [using apps script libraries with gas-fakes](libraries.md)
|
|
352
|
+
- [how libhandler works](libhandler.md)
|
|
353
|
+
- [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
|
|
351
354
|
- [named range identity](named-range-identity.md)
|
|
352
355
|
- [adc and restricted scopes](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
|
|
353
356
|
- [push test pull](pull-test-push.md)
|
package/README.md
CHANGED
|
@@ -167,6 +167,9 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
167
167
|
- [gemini](gemini-observations.md) - some reflections and experiences on using gemini to help code large projects
|
|
168
168
|
- [named colors](named-colors.md)
|
|
169
169
|
- [sandbox](sandbox.md)
|
|
170
|
+
- [using apps script libraries with gas-fakes](libraries.md)
|
|
171
|
+
- [how libhandler works](libhandler.md)
|
|
172
|
+
- [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
|
|
170
173
|
- [named range identity](named-range-identity.md)
|
|
171
174
|
- [adc and restricted scopes](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
|
|
172
175
|
- [push test pull](pull-test-push.md)
|
package/package.json
CHANGED
package/src/cli/app.js
CHANGED
|
@@ -138,9 +138,24 @@ export async function main() {
|
|
|
138
138
|
.command("enableAPIs")
|
|
139
139
|
.description("Enables or disables required Google Cloud APIs.")
|
|
140
140
|
.option("--all", "Enable all default APIs.")
|
|
141
|
+
// Drive
|
|
141
142
|
.option("--edrive", "Enable drive.googleapis.com")
|
|
142
143
|
.option("--ddrive", "Disable drive.googleapis.com")
|
|
143
|
-
//
|
|
144
|
+
// Sheets
|
|
145
|
+
.option("--esheets", "Enable sheets.googleapis.com")
|
|
146
|
+
.option("--dsheets", "Disable sheets.googleapis.com")
|
|
147
|
+
// Forms
|
|
148
|
+
.option("--eforms", "Enable forms.googleapis.com")
|
|
149
|
+
.option("--dforms", "Disable forms.googleapis.com")
|
|
150
|
+
// Docs
|
|
151
|
+
.option("--edocs", "Enable docs.googleapis.com")
|
|
152
|
+
.option("--ddocs", "Disable docs.googleapis.com")
|
|
153
|
+
// Gmail
|
|
154
|
+
.option("--egmail", "Enable gmail.googleapis.com")
|
|
155
|
+
.option("--dgmail", "Disable gmail.googleapis.com")
|
|
156
|
+
// Logging
|
|
157
|
+
.option("--elogging", "Enable logging.googleapis.com")
|
|
158
|
+
.option("--dlogging", "Disable logging.googleapis.com")
|
|
144
159
|
.action(enableGoogleAPIs);
|
|
145
160
|
|
|
146
161
|
// --- MCP Command ---
|
package/src/cli/executor.js
CHANGED
|
@@ -43,6 +43,29 @@ function generateItemWhitelistScript(items) {
|
|
|
43
43
|
return `behavior.setIdWhitelist([${whitelistItemsString}]);`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function generateGmailSandbox(gmailSandbox) {
|
|
47
|
+
if (!gmailSandbox) return [];
|
|
48
|
+
const { emailWhitelist, usageLimit, labelWhitelist, cleanup } = gmailSandbox;
|
|
49
|
+
const temp = ["const gmailSettings = behavior.sandboxService.GmailApp;"];
|
|
50
|
+
if (emailWhitelist && emailWhitelist.length > 0) {
|
|
51
|
+
temp.push(
|
|
52
|
+
`gmailSettings.emailWhitelist = ${JSON.stringify(emailWhitelist)};`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (gmailSandbox.hasOwnProperty("cleanup")) {
|
|
56
|
+
temp.push(`gmailSettings.cleanup = ${cleanup};`);
|
|
57
|
+
}
|
|
58
|
+
if (usageLimit) {
|
|
59
|
+
temp.push(`gmailSettings.usageLimit = ${usageLimit};`);
|
|
60
|
+
}
|
|
61
|
+
if (labelWhitelist && labelWhitelist.length > 0) {
|
|
62
|
+
temp.push(
|
|
63
|
+
`gmailSettings.labelWhitelist = ${JSON.stringify(labelWhitelist)};`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return temp;
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
function generateSandboxSetupScript(sandboxConfig) {
|
|
47
70
|
const script = [
|
|
48
71
|
"const behavior = ScriptApp.__behavior;",
|
|
@@ -50,11 +73,12 @@ function generateSandboxSetupScript(sandboxConfig) {
|
|
|
50
73
|
"behavior.strictSandbox = true;",
|
|
51
74
|
];
|
|
52
75
|
|
|
53
|
-
const { whitelistServices, blacklistServices, whitelistItems } =
|
|
76
|
+
const { whitelistServices, blacklistServices, whitelistItems, gmailSandbox } =
|
|
54
77
|
sandboxConfig;
|
|
55
78
|
|
|
56
79
|
script.push(...generateServiceWhitelistScript(whitelistServices));
|
|
57
80
|
script.push(...generateServiceBlacklistScript(blacklistServices));
|
|
81
|
+
script.push(...generateGmailSandbox(gmailSandbox));
|
|
58
82
|
|
|
59
83
|
const itemWhitelist = generateItemWhitelistScript(whitelistItems);
|
|
60
84
|
if (itemWhitelist) {
|
package/src/cli/mcp.js
CHANGED
|
@@ -157,7 +157,10 @@ function registerDefaultTool(server) {
|
|
|
157
157
|
if (!args.filename || !args.tools) {
|
|
158
158
|
return {
|
|
159
159
|
content: [
|
|
160
|
-
{
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: "Error: `filename` and `tools` are required.",
|
|
163
|
+
},
|
|
161
164
|
],
|
|
162
165
|
isError: true,
|
|
163
166
|
};
|
|
@@ -166,11 +169,9 @@ function registerDefaultTool(server) {
|
|
|
166
169
|
const tool_ar = [];
|
|
167
170
|
for (let i = 0; i < args.tools.length; i++) {
|
|
168
171
|
const { name, schema, gas_script, libraries } = args.tools[i];
|
|
169
|
-
// Note: This generation logic is complex; assuming gas_library is empty for generation context,
|
|
170
|
-
// or simply rendering the array strings.
|
|
171
172
|
tool_ar.push(
|
|
172
173
|
`{ name: "${name}", schema: ${schema}, func: (object = {}) => { \n\n${gas_script} }, libraries: ${JSON.stringify(
|
|
173
|
-
libraries
|
|
174
|
+
libraries || []
|
|
174
175
|
)} }`
|
|
175
176
|
);
|
|
176
177
|
}
|
package/src/cli/setup.js
CHANGED
|
@@ -2,17 +2,26 @@ import prompts from "prompts";
|
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import os from "os";
|
|
5
6
|
import { execSync } from "child_process";
|
|
6
7
|
import { checkForGcloudCli, runCommandSync } from "./utils.js";
|
|
7
8
|
|
|
8
|
-
// --- Utility
|
|
9
|
+
// --- Utility Functions ---
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively searches for .env files starting from a directory.
|
|
13
|
+
* @param {string} dir - Start directory
|
|
14
|
+
* @returns {Promise<string[]>} List of found .env file paths
|
|
15
|
+
*/
|
|
9
16
|
async function findEnvFiles(dir) {
|
|
10
17
|
try {
|
|
11
18
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
12
19
|
const promises = entries.map((entry) => {
|
|
13
20
|
const fullPath = path.join(dir, entry.name);
|
|
14
21
|
if (entry.isDirectory()) {
|
|
15
|
-
if (entry.name === "node_modules")
|
|
22
|
+
if (entry.name === "node_modules") {
|
|
23
|
+
return Promise.resolve([]);
|
|
24
|
+
}
|
|
16
25
|
return findEnvFiles(fullPath);
|
|
17
26
|
} else if (entry.isFile() && entry.name === ".env") {
|
|
18
27
|
return Promise.resolve(fullPath);
|
|
@@ -27,7 +36,7 @@ async function findEnvFiles(dir) {
|
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
// ---
|
|
39
|
+
// --- Exported Command Implementations ---
|
|
31
40
|
|
|
32
41
|
export async function initializeConfiguration(options = {}) {
|
|
33
42
|
let envPath;
|
|
@@ -38,7 +47,10 @@ export async function initializeConfiguration(options = {}) {
|
|
|
38
47
|
} else {
|
|
39
48
|
const foundFiles = await findEnvFiles(process.cwd());
|
|
40
49
|
if (foundFiles.length > 0) {
|
|
41
|
-
const choices = foundFiles.map((file) => ({
|
|
50
|
+
const choices = foundFiles.map((file) => ({
|
|
51
|
+
title: file,
|
|
52
|
+
value: file,
|
|
53
|
+
}));
|
|
42
54
|
choices.push({
|
|
43
55
|
title: "Create a new .env file in the current directory",
|
|
44
56
|
value: "new",
|
|
@@ -56,10 +68,11 @@ export async function initializeConfiguration(options = {}) {
|
|
|
56
68
|
return;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
if (response.envPathSelection === "new") {
|
|
72
|
+
envPath = path.join(process.cwd(), ".env");
|
|
73
|
+
} else {
|
|
74
|
+
envPath = response.envPathSelection;
|
|
75
|
+
}
|
|
63
76
|
} else {
|
|
64
77
|
console.log(
|
|
65
78
|
"No .env file found. A new one will be created in the current directory."
|
|
@@ -79,15 +92,18 @@ export async function initializeConfiguration(options = {}) {
|
|
|
79
92
|
|
|
80
93
|
console.log("--------------------------------------------------");
|
|
81
94
|
console.log("Configuring .env for gas-fakes");
|
|
95
|
+
console.log("Press Enter to accept the default value in brackets.");
|
|
96
|
+
console.log("Use Space to select/deselect scopes.");
|
|
82
97
|
console.log("--------------------------------------------------");
|
|
83
98
|
|
|
84
99
|
const existingExtraScopes = existingConfig.EXTRA_SCOPES
|
|
85
100
|
? existingConfig.EXTRA_SCOPES.split(",").filter((s) => s)
|
|
86
101
|
: [];
|
|
102
|
+
|
|
87
103
|
const responses = {};
|
|
88
104
|
|
|
89
|
-
// Stage 1: Basic Info
|
|
90
|
-
const
|
|
105
|
+
// --- Stage 1: Basic Info ---
|
|
106
|
+
const basicInfoQuestions = [
|
|
91
107
|
{
|
|
92
108
|
type: "text",
|
|
93
109
|
name: "GCP_PROJECT_ID",
|
|
@@ -102,17 +118,26 @@ export async function initializeConfiguration(options = {}) {
|
|
|
102
118
|
"Enter a test Drive file ID for authentication checks (optional)",
|
|
103
119
|
initial: existingConfig.DRIVE_TEST_FILE_ID || "",
|
|
104
120
|
},
|
|
105
|
-
]
|
|
121
|
+
];
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
const basicInfoResponses = await prompts(basicInfoQuestions);
|
|
124
|
+
if (typeof basicInfoResponses.GCP_PROJECT_ID === "undefined") {
|
|
125
|
+
console.log("Initialization cancelled.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
108
128
|
Object.assign(responses, basicInfoResponses);
|
|
109
129
|
|
|
110
|
-
// Stage 2: Scopes
|
|
130
|
+
// --- Stage 2: Scopes ---
|
|
131
|
+
|
|
111
132
|
const DEFAULT_SCOPES_VALUES = [
|
|
112
133
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
113
134
|
"openid",
|
|
114
135
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
115
136
|
];
|
|
137
|
+
console.log(
|
|
138
|
+
"\nThe following default scopes are required for basic operations and will be enabled automatically:"
|
|
139
|
+
);
|
|
140
|
+
DEFAULT_SCOPES_VALUES.forEach((scope) => console.log(` - ${scope}`));
|
|
116
141
|
responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES;
|
|
117
142
|
|
|
118
143
|
const extraScopeQuestion = {
|
|
@@ -124,7 +149,30 @@ export async function initializeConfiguration(options = {}) {
|
|
|
124
149
|
title: "Workspace resources",
|
|
125
150
|
value: "https://www.googleapis.com/auth/drive",
|
|
126
151
|
},
|
|
152
|
+
/*
|
|
153
|
+
{
|
|
154
|
+
title: "Sheets (full access)",
|
|
155
|
+
value: "https://www.googleapis.com/auth/spreadsheets",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
title: "Docs (full access)",
|
|
159
|
+
value: "https://www.googleapis.com/auth/documents",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: "Forms (full access)",
|
|
163
|
+
value: "https://www.googleapis.com/auth/forms",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
title: "Gmail (send mail)",
|
|
167
|
+
value: "https://www.googleapis.com/auth/gmail.send",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
title: "Gmail (full access)",
|
|
171
|
+
value: "https://www.googleapis.com/auth/gmail.modify",
|
|
172
|
+
},
|
|
173
|
+
*/
|
|
127
174
|
{
|
|
175
|
+
// actually labels are not sensitive
|
|
128
176
|
title: "Gmail labels",
|
|
129
177
|
value: "https://www.googleapis.com/auth/gmail.labels",
|
|
130
178
|
},
|
|
@@ -133,49 +181,130 @@ export async function initializeConfiguration(options = {}) {
|
|
|
133
181
|
title: "Gmail compose",
|
|
134
182
|
value: "https://www.googleapis.com/auth/gmail.compose",
|
|
135
183
|
},
|
|
184
|
+
{
|
|
185
|
+
sensitivity: "sensitive",
|
|
186
|
+
title: "Gmail modify",
|
|
187
|
+
value: "https://www.googleapis.com/auth/gmail.modify",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
sensitivity: "sensitive",
|
|
191
|
+
title: "Gmail send",
|
|
192
|
+
value: "https://www.googleapis.com/auth/gmail.send",
|
|
193
|
+
},
|
|
136
194
|
].map((scope) => ({
|
|
137
195
|
...scope,
|
|
138
196
|
title: scope.sensitivity
|
|
139
197
|
? `[${scope.sensitivity}] ${scope.title}`
|
|
140
198
|
: scope.title,
|
|
199
|
+
// because we always need drive for ant extra scopes
|
|
141
200
|
selected:
|
|
142
201
|
existingExtraScopes.length > 0
|
|
143
202
|
? existingExtraScopes.includes(scope.value)
|
|
144
203
|
: scope.value === "https://www.googleapis.com/auth/drive",
|
|
145
204
|
})),
|
|
205
|
+
hint: "- Use space to select/deselect. Press Enter to submit.",
|
|
146
206
|
};
|
|
147
207
|
|
|
208
|
+
// Check for any kind of sensitivity
|
|
209
|
+
const sensitiveScopesList = extraScopeQuestion.choices.filter(
|
|
210
|
+
(scope) => scope.sensitivity
|
|
211
|
+
);
|
|
212
|
+
|
|
148
213
|
const extraScopeResponses = await prompts(extraScopeQuestion);
|
|
149
|
-
|
|
214
|
+
|
|
215
|
+
if (typeof extraScopeResponses.EXTRA_SCOPES === "undefined") {
|
|
216
|
+
console.log("Initialization cancelled.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
150
219
|
Object.assign(responses, extraScopeResponses);
|
|
151
220
|
|
|
152
|
-
const
|
|
221
|
+
const selectedExtraScopes = responses.EXTRA_SCOPES || [];
|
|
222
|
+
|
|
223
|
+
const usesSensitiveScopes = sensitiveScopesList.some((s) =>
|
|
224
|
+
selectedExtraScopes.includes(s.value)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (usesSensitiveScopes) {
|
|
228
|
+
console.log("\n--------------------------------------------------");
|
|
229
|
+
console.log(
|
|
230
|
+
"You have selected sensitive or restricted scopes. Google requires an OAuth client credential file for these."
|
|
231
|
+
);
|
|
232
|
+
console.log(
|
|
233
|
+
"See the getting started guide https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md for how."
|
|
234
|
+
);
|
|
235
|
+
console.log("--------------------------------------------------");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const clientCredentialQuestion = {
|
|
153
239
|
type: "text",
|
|
154
240
|
name: "CLIENT_CREDENTIAL_FILE",
|
|
155
|
-
message:
|
|
241
|
+
message: usesSensitiveScopes
|
|
242
|
+
? "Enter the path and filename for your OAuth client credentials JSON"
|
|
243
|
+
: "Enter path to OAuth client credentials JSON (optional)",
|
|
156
244
|
initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
|
|
157
|
-
|
|
245
|
+
validate: (input) => {
|
|
246
|
+
const trimmedInput = input.trim();
|
|
247
|
+
|
|
248
|
+
if (usesSensitiveScopes) {
|
|
249
|
+
if (trimmedInput === "") {
|
|
250
|
+
return "This field is required for the selected sensitive scopes.";
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
if (trimmedInput === "") {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const resolvedPath = path.resolve(process.cwd(), trimmedInput);
|
|
259
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
260
|
+
return `File not found at '${resolvedPath}'. Please check the path and try again.`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const clientCredentialResponse = await prompts(clientCredentialQuestion);
|
|
268
|
+
if (typeof clientCredentialResponse.CLIENT_CREDENTIAL_FILE === "undefined") {
|
|
269
|
+
console.log("Initialization cancelled.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
158
272
|
Object.assign(responses, clientCredentialResponse);
|
|
159
273
|
|
|
160
|
-
// Stage 3: Remaining Config
|
|
161
|
-
const
|
|
274
|
+
// --- Stage 3: Remaining Config ---
|
|
275
|
+
const defaultScopesDisplay = `\n - Default: [${responses.DEFAULT_SCOPES.join(
|
|
276
|
+
", "
|
|
277
|
+
)}]`;
|
|
278
|
+
const extraScopesDisplay =
|
|
279
|
+
responses.EXTRA_SCOPES && responses.EXTRA_SCOPES.length > 0
|
|
280
|
+
? `\n - Extra: [${responses.EXTRA_SCOPES.join(", ")}]`
|
|
281
|
+
: "\n - Extra: [None]";
|
|
282
|
+
|
|
283
|
+
const remainingQuestions = [
|
|
162
284
|
{
|
|
163
285
|
type: "toggle",
|
|
164
286
|
name: "QUIET",
|
|
165
287
|
message: "Run gas-fakes package in quiet mode",
|
|
166
|
-
initial: existingConfig.QUIET === "true",
|
|
288
|
+
initial: existingConfig.QUIET === "true" ? true : false,
|
|
167
289
|
},
|
|
168
290
|
{
|
|
169
291
|
type: "select",
|
|
170
292
|
name: "LOG_DESTINATION",
|
|
171
|
-
message:
|
|
293
|
+
message: `Selected Scopes:${defaultScopesDisplay}${extraScopesDisplay}\n\nEnter logging destination`,
|
|
172
294
|
choices: [
|
|
173
295
|
{ title: "CONSOLE", value: "CONSOLE" },
|
|
174
296
|
{ title: "CLOUD", value: "CLOUD" },
|
|
175
297
|
{ title: "BOTH", value: "BOTH" },
|
|
176
298
|
{ title: "NONE", value: "NONE" },
|
|
177
299
|
],
|
|
178
|
-
initial:
|
|
300
|
+
initial:
|
|
301
|
+
["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
|
|
302
|
+
existingConfig.LOG_DESTINATION
|
|
303
|
+
) > -1
|
|
304
|
+
? ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
|
|
305
|
+
existingConfig.LOG_DESTINATION
|
|
306
|
+
)
|
|
307
|
+
: 0,
|
|
179
308
|
},
|
|
180
309
|
{
|
|
181
310
|
type: "select",
|
|
@@ -185,18 +314,31 @@ export async function initializeConfiguration(options = {}) {
|
|
|
185
314
|
{ title: "FILE", value: "FILE" },
|
|
186
315
|
{ title: "UPSTASH", value: "UPSTASH" },
|
|
187
316
|
],
|
|
188
|
-
initial:
|
|
317
|
+
initial:
|
|
318
|
+
["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE?.toUpperCase()) >
|
|
319
|
+
-1
|
|
320
|
+
? ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE.toUpperCase())
|
|
321
|
+
: 0,
|
|
189
322
|
},
|
|
190
|
-
]
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
const remainingResponses = await prompts(remainingQuestions);
|
|
326
|
+
if (typeof remainingResponses.LOG_DESTINATION === "undefined") {
|
|
327
|
+
console.log("Initialization cancelled.");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
191
330
|
Object.assign(responses, remainingResponses);
|
|
192
331
|
|
|
193
|
-
|
|
332
|
+
// Convert scope arrays to comma-separated strings for saving
|
|
333
|
+
if (Array.isArray(responses.DEFAULT_SCOPES)) {
|
|
194
334
|
responses.DEFAULT_SCOPES = responses.DEFAULT_SCOPES.join(",");
|
|
195
|
-
|
|
335
|
+
}
|
|
336
|
+
if (Array.isArray(responses.EXTRA_SCOPES)) {
|
|
196
337
|
responses.EXTRA_SCOPES = responses.EXTRA_SCOPES.join(",");
|
|
338
|
+
}
|
|
197
339
|
|
|
198
340
|
if (responses.STORE_TYPE === "UPSTASH") {
|
|
199
|
-
const
|
|
341
|
+
const upstashQuestions = [
|
|
200
342
|
{
|
|
201
343
|
type: "text",
|
|
202
344
|
name: "UPSTASH_REDIS_REST_URL",
|
|
@@ -209,10 +351,23 @@ export async function initializeConfiguration(options = {}) {
|
|
|
209
351
|
message: "Enter your Upstash Redis REST Token",
|
|
210
352
|
initial: existingConfig.UPSTASH_REDIS_REST_TOKEN || "",
|
|
211
353
|
},
|
|
212
|
-
]
|
|
354
|
+
];
|
|
355
|
+
const upstashResponses = await prompts(upstashQuestions);
|
|
356
|
+
|
|
357
|
+
if (typeof upstashResponses.UPSTASH_REDIS_REST_URL === "undefined") {
|
|
358
|
+
console.log("Initialization cancelled during Upstash configuration.");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
213
361
|
Object.assign(responses, upstashResponses);
|
|
214
362
|
}
|
|
215
363
|
|
|
364
|
+
// --- Confirmation Step ---
|
|
365
|
+
console.log("\n------------------ Summary ------------------");
|
|
366
|
+
Object.entries(responses).forEach(([key, value]) => {
|
|
367
|
+
if (value !== undefined) console.log(`${key}: ${value}`);
|
|
368
|
+
});
|
|
369
|
+
console.log("-------------------------------------------");
|
|
370
|
+
|
|
216
371
|
const confirmSave = await prompts({
|
|
217
372
|
type: "confirm",
|
|
218
373
|
name: "save",
|
|
@@ -221,33 +376,51 @@ export async function initializeConfiguration(options = {}) {
|
|
|
221
376
|
});
|
|
222
377
|
|
|
223
378
|
if (!confirmSave.save) {
|
|
224
|
-
console.log("Configuration discarded.");
|
|
379
|
+
console.log("Configuration discarded. No changes were made.");
|
|
225
380
|
return;
|
|
226
381
|
}
|
|
227
382
|
|
|
383
|
+
// --- File Writing Logic ---
|
|
384
|
+
console.log(`Writing configuration to "${envPath}"...`);
|
|
228
385
|
const inits =
|
|
229
386
|
responses.STORE_TYPE !== "UPSTASH"
|
|
230
387
|
? { UPSTASH_REDIS_REST_TOKEN: "", UPSTASH_REDIS_REST_URL: "" }
|
|
231
388
|
: {};
|
|
232
389
|
const finalConfig = { ...existingConfig, ...responses, ...inits };
|
|
233
390
|
|
|
391
|
+
console.log("\n------------------ Final output ------------------");
|
|
234
392
|
const envContent = Reflect.ownKeys(finalConfig)
|
|
235
|
-
.map((key) =>
|
|
393
|
+
.map((key) => {
|
|
394
|
+
const item = finalConfig[key];
|
|
395
|
+
const res = `${key}="${(item.toString() || "").trim()}"`;
|
|
396
|
+
console.log(res);
|
|
397
|
+
return res;
|
|
398
|
+
})
|
|
236
399
|
.join("\n");
|
|
237
400
|
|
|
238
401
|
fs.writeFileSync(envPath, envContent + "\n", "utf8");
|
|
402
|
+
|
|
403
|
+
console.log("--------------------------------------------------");
|
|
239
404
|
console.log("Setup complete. Your .env file has been updated.");
|
|
405
|
+
console.log("--------------------------------------------------");
|
|
240
406
|
}
|
|
241
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Handles the 'auth' command to authenticate with Google Cloud.
|
|
410
|
+
*/
|
|
242
411
|
export function authenticateUser() {
|
|
412
|
+
// First, check if gcloud CLI is available.
|
|
243
413
|
checkForGcloudCli();
|
|
244
|
-
|
|
414
|
+
|
|
415
|
+
const rootDirectory = process.cwd();
|
|
416
|
+
const envPath = path.join(rootDirectory, ".env");
|
|
245
417
|
|
|
246
418
|
if (!fs.existsSync(envPath)) {
|
|
247
419
|
console.error(`Error: .env file not found at '${envPath}'`);
|
|
248
420
|
console.error("Please run './gas-fakes.js init' first.");
|
|
249
421
|
process.exit(1);
|
|
250
422
|
}
|
|
423
|
+
|
|
251
424
|
dotenv.config({ path: envPath, quiet: true });
|
|
252
425
|
|
|
253
426
|
const {
|
|
@@ -263,42 +436,149 @@ export function authenticateUser() {
|
|
|
263
436
|
process.exit(1);
|
|
264
437
|
}
|
|
265
438
|
|
|
266
|
-
|
|
439
|
+
const defaultScopes =
|
|
267
440
|
DEFAULT_SCOPES ||
|
|
268
441
|
"https://www.googleapis.com/auth/userinfo.email,openid,https://www.googleapis.com/auth/cloud-platform";
|
|
269
|
-
|
|
270
|
-
|
|
442
|
+
const extraScopes =
|
|
443
|
+
EXTRA_SCOPES ||
|
|
444
|
+
"https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/spreadsheets";
|
|
445
|
+
|
|
446
|
+
let scopes = defaultScopes;
|
|
447
|
+
if (extraScopes && extraScopes.length > 0) {
|
|
448
|
+
scopes += (extraScopes.startsWith(",") ? "" : ",") + extraScopes;
|
|
271
449
|
}
|
|
272
450
|
|
|
451
|
+
const driveAccessFlag = "--enable-gdrive-access";
|
|
452
|
+
|
|
453
|
+
console.log(`...requesting scopes ${scopes}`);
|
|
454
|
+
|
|
273
455
|
let clientFlag = "";
|
|
274
|
-
if (CLIENT_CREDENTIAL_FILE
|
|
275
|
-
|
|
456
|
+
if (CLIENT_CREDENTIAL_FILE) {
|
|
457
|
+
console.log("...attempting to use enhanced client credentials");
|
|
458
|
+
|
|
459
|
+
let clientPath = CLIENT_CREDENTIAL_FILE;
|
|
460
|
+
if (!path.isAbsolute(clientPath)) {
|
|
461
|
+
clientPath = path.join(rootDirectory, clientPath);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (fs.existsSync(clientPath)) {
|
|
465
|
+
clientFlag = `--client-id-file="${clientPath}"`;
|
|
466
|
+
} else {
|
|
467
|
+
console.error(
|
|
468
|
+
`Error: Client credential file specified in .env not found at '${clientPath}'`
|
|
469
|
+
);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
console.log(
|
|
474
|
+
"\n...CLIENT_CREDENTIAL_FILE is not set. Using default Application Default Credentials (ADC)."
|
|
475
|
+
);
|
|
476
|
+
console.log(
|
|
477
|
+
"...if you have requested any sensitive scopes, you'll see 'This app is blocked message.'"
|
|
478
|
+
);
|
|
479
|
+
console.log(
|
|
480
|
+
"...To allow them see - https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md\n"
|
|
481
|
+
);
|
|
276
482
|
}
|
|
277
483
|
|
|
484
|
+
const projectId = GCP_PROJECT_ID;
|
|
278
485
|
const activeConfig = AC || "default";
|
|
279
486
|
|
|
487
|
+
console.log("Revoking previous credentials...");
|
|
280
488
|
try {
|
|
281
489
|
execSync("gcloud auth revoke --quiet", { stdio: "ignore" });
|
|
282
|
-
} catch (e) {
|
|
283
|
-
|
|
490
|
+
} catch (e) {
|
|
491
|
+
/* ignore */
|
|
492
|
+
}
|
|
284
493
|
try {
|
|
285
|
-
|
|
494
|
+
execSync("gcloud auth application-default revoke --quiet", {
|
|
495
|
+
stdio: "ignore",
|
|
496
|
+
});
|
|
286
497
|
} catch (e) {
|
|
498
|
+
/* ignore */
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
console.log(`Ensuring gcloud configuration '${activeConfig}' exists...`);
|
|
502
|
+
try {
|
|
503
|
+
execSync(`gcloud config configurations describe "${activeConfig}"`, {
|
|
504
|
+
stdio: "ignore",
|
|
505
|
+
});
|
|
506
|
+
console.log(`Configuration '${activeConfig}' already exists.`);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.log(`Configuration '${activeConfig}' not found. Creating it...`);
|
|
287
509
|
runCommandSync(`gcloud config configurations create "${activeConfig}"`);
|
|
288
|
-
runCommandSync(`gcloud config configurations activate "${activeConfig}"`);
|
|
289
510
|
}
|
|
290
511
|
|
|
291
|
-
|
|
292
|
-
runCommandSync(`gcloud
|
|
512
|
+
console.log(`Activating gcloud configuration: ${activeConfig}`);
|
|
513
|
+
runCommandSync(`gcloud config configurations activate "${activeConfig}"`);
|
|
514
|
+
|
|
515
|
+
console.log(`Setting project to: ${projectId}`);
|
|
516
|
+
runCommandSync(`gcloud config set project ${projectId}`);
|
|
517
|
+
runCommandSync(`gcloud config set billing/quota_project ${projectId}`);
|
|
518
|
+
|
|
519
|
+
console.log("Initiating user login...");
|
|
520
|
+
runCommandSync(`gcloud auth login ${driveAccessFlag}`);
|
|
521
|
+
|
|
522
|
+
console.log("Initiating Application Default Credentials (ADC) login...");
|
|
293
523
|
runCommandSync(
|
|
294
524
|
`gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`
|
|
295
525
|
);
|
|
526
|
+
runCommandSync(
|
|
527
|
+
`gcloud auth application-default set-quota-project ${projectId}`
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// --- Verification ---
|
|
531
|
+
console.log("\nVerifying configuration...");
|
|
296
532
|
|
|
533
|
+
const gcloudConfigDir =
|
|
534
|
+
process.env.CLOUDSDK_CONFIG || path.join(os.homedir(), ".config", "gcloud");
|
|
535
|
+
const activeConfigPath = path.join(gcloudConfigDir, "active_config");
|
|
536
|
+
|
|
537
|
+
let currentConfig = "unknown";
|
|
538
|
+
if (fs.existsSync(activeConfigPath)) {
|
|
539
|
+
currentConfig = fs.readFileSync(activeConfigPath, "utf8").trim();
|
|
540
|
+
} else {
|
|
541
|
+
console.warn(
|
|
542
|
+
`Warning: Could not find active_config file at ${activeConfigPath}`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const currentProject = execSync("gcloud config get project")
|
|
547
|
+
.toString()
|
|
548
|
+
.trim();
|
|
549
|
+
console.log(
|
|
550
|
+
`Active config is ${currentConfig} - project is ${currentProject}`
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
console.log("\nFetching token information...");
|
|
554
|
+
const userToken = execSync("gcloud auth print-access-token")
|
|
555
|
+
.toString()
|
|
556
|
+
.trim();
|
|
557
|
+
const appDefaultToken = execSync(
|
|
558
|
+
"gcloud auth application-default print-access-token"
|
|
559
|
+
)
|
|
560
|
+
.toString()
|
|
561
|
+
.trim();
|
|
562
|
+
|
|
563
|
+
console.log("\n...user token scopes");
|
|
564
|
+
runCommandSync(
|
|
565
|
+
`curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${userToken}`
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
console.log("\n...application default token scopes");
|
|
569
|
+
runCommandSync(
|
|
570
|
+
`curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${appDefaultToken}`
|
|
571
|
+
);
|
|
297
572
|
console.log("\nAuthentication process finished.");
|
|
298
573
|
}
|
|
299
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Handles the 'enableAPIs' command to enable or disable necessary Google Cloud services based on options.
|
|
577
|
+
* @param {object} options Options object provided by commander.js.
|
|
578
|
+
*/
|
|
300
579
|
export function enableGoogleAPIs(options) {
|
|
301
580
|
checkForGcloudCli();
|
|
581
|
+
|
|
302
582
|
const API_SERVICES = {
|
|
303
583
|
drive: "drive.googleapis.com",
|
|
304
584
|
sheets: "sheets.googleapis.com",
|
|
@@ -308,22 +588,45 @@ export function enableGoogleAPIs(options) {
|
|
|
308
588
|
logging: "logging.googleapis.com",
|
|
309
589
|
};
|
|
310
590
|
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
591
|
+
const servicesToEnable = new Set();
|
|
592
|
+
const servicesToDisable = new Set();
|
|
314
593
|
if (options.all || Object.keys(options).length === 0) {
|
|
315
|
-
|
|
594
|
+
Object.values(API_SERVICES).forEach((service) =>
|
|
595
|
+
servicesToEnable.add(service)
|
|
596
|
+
);
|
|
316
597
|
} else {
|
|
317
598
|
for (const key in API_SERVICES) {
|
|
318
|
-
if (options[`e${key}`])
|
|
319
|
-
|
|
599
|
+
if (options[`e${key}`]) {
|
|
600
|
+
servicesToEnable.add(API_SERVICES[key]);
|
|
601
|
+
}
|
|
602
|
+
if (options[`d${key}`]) {
|
|
603
|
+
servicesToDisable.add(API_SERVICES[key]);
|
|
604
|
+
}
|
|
320
605
|
}
|
|
321
606
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
607
|
+
if (servicesToEnable.size > 0) {
|
|
608
|
+
const enableList = Array.from(servicesToEnable);
|
|
609
|
+
console.log(`Enabling Google Cloud services: ${enableList.join(", ")}...`);
|
|
610
|
+
runCommandSync(`gcloud services enable ${enableList.join(" ")}`);
|
|
611
|
+
console.log("Services enabled successfully.");
|
|
325
612
|
}
|
|
326
|
-
if (
|
|
327
|
-
|
|
613
|
+
if (servicesToDisable.size > 0) {
|
|
614
|
+
const disableList = Array.from(servicesToDisable);
|
|
615
|
+
console.log(
|
|
616
|
+
`Disabling Google Cloud services: ${disableList.join(", ")}...`
|
|
617
|
+
);
|
|
618
|
+
runCommandSync(`gcloud services disable ${disableList.join(" ")}`);
|
|
619
|
+
console.log("Services disabled successfully.");
|
|
620
|
+
}
|
|
621
|
+
if (
|
|
622
|
+
servicesToEnable.size === 0 &&
|
|
623
|
+
servicesToDisable.size === 0 &&
|
|
624
|
+
Object.keys(options).length > 0 &&
|
|
625
|
+
!options.all
|
|
626
|
+
) {
|
|
627
|
+
console.log("No specific APIs were selected to enable or disable.");
|
|
628
|
+
console.log(
|
|
629
|
+
"Use '--all' to enable all default APIs, or specify flags like '--edrive' or '--ddrive'."
|
|
630
|
+
);
|
|
328
631
|
}
|
|
329
632
|
}
|
package/src/cli/utils.js
CHANGED
|
@@ -5,8 +5,8 @@ const require = createRequire(import.meta.url);
|
|
|
5
5
|
const pjson = require("../../package.json");
|
|
6
6
|
|
|
7
7
|
export const VERSION = pjson.version;
|
|
8
|
-
export const CLI_VERSION = "0.0.
|
|
9
|
-
export const MCP_VERSION = "0.0.
|
|
8
|
+
export const CLI_VERSION = "0.0.18"; // Kept from original logic
|
|
9
|
+
export const MCP_VERSION = "0.0.7";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Replaces escaped newline characters ('\\n') with actual newlines,
|
package/src/index.js
CHANGED
|
@@ -64,6 +64,7 @@ class FakeAdvDriveFiles {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
+
* TODO implement the correct download method
|
|
67
68
|
* this is fairly pointless in apps script as it returns an operation, and Drive.Operations are not supported
|
|
68
69
|
* TODO - look into what actually happens to the operation - it may be possible to do something with it using the operations ap directly
|
|
69
70
|
* for the moment we'll just return a fake operation that looks like adv returns
|
|
@@ -193,48 +194,24 @@ class FakeAdvDriveFiles {
|
|
|
193
194
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
export() {
|
|
197
|
-
return notYetImplemented()
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
}
|
|
197
|
+
export(fileId, mimeType) {
|
|
201
198
|
|
|
199
|
+
if (!is.nonEmptyString(fileId)) {
|
|
200
|
+
throw new Error(`API call to drive.files.export failed with error: Required`)
|
|
201
|
+
}
|
|
202
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'read');
|
|
203
|
+
const params = {
|
|
204
|
+
id: fileId,
|
|
205
|
+
mimeType,
|
|
206
|
+
}
|
|
202
207
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
* @param {string} [p.fields=minFields] which fields to get
|
|
207
|
-
* @return {string[]} an array of the fields required merged with the minimum fields required to support caching
|
|
208
|
-
*/
|
|
209
|
-
const tidyFieldsFar = ({ fields = "" } = {}, mf = minFields) => {
|
|
210
|
-
if (!is.string(fields)) {
|
|
211
|
-
throw new Error(`invalid fields definition`, fields)
|
|
208
|
+
const { response, data } = Syncit.fxDriveExport(params)
|
|
209
|
+
return data
|
|
210
|
+
|
|
212
211
|
}
|
|
213
212
|
|
|
214
|
-
return Array.from(
|
|
215
|
-
new Set((mf.split(",").concat(fields.split(","))
|
|
216
|
-
.map(f => f.replace(/\s/g, ""))
|
|
217
|
-
.filter(f => f)))
|
|
218
|
-
.keys()
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* enhance the array of required fields by adding any propertyies already in cache
|
|
225
|
-
* @param {object} p
|
|
226
|
-
* @param {File} p.cachedFile meta data
|
|
227
|
-
* @returns {string} an enhanced fields as a string with the dedupped fields already in cache
|
|
228
|
-
*/
|
|
229
|
-
const enhanceFar = ({ cachedFile, far }) => {
|
|
230
|
-
// we'll enhance the cache with the current value of any already fetched key by fetching it again
|
|
231
|
-
far = cachedFile ? Array.from(new Set(far.concat(Reflect.ownKeys(cachedFile))).keys()) : far
|
|
232
|
-
|
|
233
|
-
// now construct an appropriate fields arg
|
|
234
|
-
return far.join(",")
|
|
235
213
|
}
|
|
236
214
|
|
|
237
|
-
|
|
238
215
|
/**
|
|
239
216
|
* ceate/patch a file and optionally upload some data
|
|
240
217
|
* update and copy are virtually the same payload
|
|
@@ -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 { newFakeLibHandlerApp as maker} from './fakelibhandlerapp.js';
|
|
6
|
+
import { lazyLoaderApp } from '../common/lazyloader.js'
|
|
7
|
+
|
|
8
|
+
let _app = null;
|
|
9
|
+
_app = lazyLoaderApp(_app, 'LibHandlerApp', maker)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { Auth } from '../../support/auth.js';
|
|
3
|
+
import { newFakeLibrary } from './fakelibrary.js';
|
|
4
|
+
export const newFakeLibHandler = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeLibHandler(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// to keep the same pattern as other apps script services, we'll use the worker to async/sync
|
|
9
|
+
// the starting point is the current manifest (or another manifest if specified)
|
|
10
|
+
class FakeLibHandler {
|
|
11
|
+
constructor(manifest) {
|
|
12
|
+
this.__manifest = manifest
|
|
13
|
+
if (!this.__manifest) {
|
|
14
|
+
console.warn ('...manifest not found in auth and not provided - no libraries will be loaded');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get manifest() {
|
|
19
|
+
return this.__manifest;
|
|
20
|
+
}
|
|
21
|
+
get dependencies() {
|
|
22
|
+
return this.__manifest.dependencies;
|
|
23
|
+
}
|
|
24
|
+
get libraries() {
|
|
25
|
+
return this.dependencies?.libraries;
|
|
26
|
+
}
|
|
27
|
+
get enabledAdvancedServices() {
|
|
28
|
+
return this.dependencies?.enabledAdvancedServices;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fetchLibraries() {
|
|
32
|
+
const libs = this.dependencies?.libraries.map(newFakeLibrary);
|
|
33
|
+
return libs
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toString() {
|
|
37
|
+
return 'LibHandler';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { Auth } from '../../support/auth.js';
|
|
3
|
+
import { newFakeLibHandler } from './fakelibhandler.js';
|
|
4
|
+
|
|
5
|
+
export const newFakeLibHandlerApp = (...args) => {
|
|
6
|
+
return Proxies.guard(new FakeLibHandlerApp(...args));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// we ned to be able to handle multiple recursive libraries
|
|
10
|
+
// the default source will be the current manifest
|
|
11
|
+
// for now we only sipport the HEAD version of libraries
|
|
12
|
+
class FakeLibHandlerApp {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.__libMap = new Map()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get libMap() {
|
|
18
|
+
return this.__libMap
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
load(manifest) {
|
|
23
|
+
manifest = manifest || Auth.getManifest();
|
|
24
|
+
if (!manifest) {
|
|
25
|
+
throw new Error('manifest not found in auth and not provided');
|
|
26
|
+
}
|
|
27
|
+
this.__libMap = new Map()
|
|
28
|
+
|
|
29
|
+
const recurseManifests = (manifest) => {
|
|
30
|
+
const libs = newFakeLibHandler(manifest).fetchLibraries();
|
|
31
|
+
libs.forEach((lib) => {
|
|
32
|
+
if (!this.libMap.has(lib.libraryId)) {
|
|
33
|
+
this.libMap.set(lib.libraryId, lib);
|
|
34
|
+
console.log(`...loading ${lib.libraryId} - ${lib.userSymbol}`)
|
|
35
|
+
if (lib.libraries) {
|
|
36
|
+
recurseManifests(lib.manifest)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
recurseManifests(manifest)
|
|
42
|
+
|
|
43
|
+
this.libMap.forEach((lib) => {
|
|
44
|
+
lib.inject()
|
|
45
|
+
})
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
toString() {
|
|
51
|
+
return 'LibHandlerApp';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { parse } from 'acorn';
|
|
3
|
+
|
|
4
|
+
export const newFakeLibrary = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeLibrary(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// to keep the same pattern as other apps script services, we'll use the worker to async/sync
|
|
9
|
+
// the starting point is the current manifest (or another manifest if specified)
|
|
10
|
+
class FakeLibrary {
|
|
11
|
+
constructor(libraryOb) {
|
|
12
|
+
this.__libraryOb = libraryOb;
|
|
13
|
+
if (!this.__libraryOb) {
|
|
14
|
+
throw new Error('library not provided');
|
|
15
|
+
}
|
|
16
|
+
this.__content = null;
|
|
17
|
+
this.__libContent = null;
|
|
18
|
+
this.__manifest = null;
|
|
19
|
+
}
|
|
20
|
+
get libraryOb() {
|
|
21
|
+
return this.__libraryOb;
|
|
22
|
+
}
|
|
23
|
+
get version() {
|
|
24
|
+
return this.__libraryOb.version;
|
|
25
|
+
}
|
|
26
|
+
get userSymbol() {
|
|
27
|
+
return this.__libraryOb.userSymbol;
|
|
28
|
+
}
|
|
29
|
+
get libraryId() {
|
|
30
|
+
if (!this.__libraryOb.libraryId) {
|
|
31
|
+
throw new Error(`libraryId not found in library ${JSON.stringify(this.__libraryOb)}`);
|
|
32
|
+
}
|
|
33
|
+
return this.__libraryOb.libraryId;
|
|
34
|
+
}
|
|
35
|
+
get libContent() {
|
|
36
|
+
if (!this.__libContent) {
|
|
37
|
+
this.__allowSandboxAccess();
|
|
38
|
+
const data = Drive.Files.export(
|
|
39
|
+
this.libraryId,
|
|
40
|
+
'application/vnd.google-apps.script+json',
|
|
41
|
+
);
|
|
42
|
+
if (!data) {
|
|
43
|
+
throw new Error(`Library ${this.libraryId} not found`);
|
|
44
|
+
}
|
|
45
|
+
this.__libContent = JSON.parse(Utilities.newBlob(data).getDataAsString())
|
|
46
|
+
}
|
|
47
|
+
return this.__libContent
|
|
48
|
+
}
|
|
49
|
+
__allowSandboxAccess() {
|
|
50
|
+
if (ScriptApp.__behavior?.sandboxMode) {
|
|
51
|
+
ScriptApp.__behavior.addWhitelistedFile(this.libraryId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get combinedJs() {
|
|
56
|
+
return this.libContent.files.filter((f) => f.type === 'server_js').map((f) => `////-- ${f.name} --\n${f.source}`).join(`\n\n`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get content() {
|
|
60
|
+
if (!this.__content) {
|
|
61
|
+
const libContent = this.libContent;
|
|
62
|
+
this.__content = {
|
|
63
|
+
...this.__libraryOb,
|
|
64
|
+
...libContent,
|
|
65
|
+
serverJs: this.serverJs,
|
|
66
|
+
manifest: this.manifest,
|
|
67
|
+
libraries: this.libraries,
|
|
68
|
+
combinedJs: this.combinedJs
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return this.__content
|
|
72
|
+
}
|
|
73
|
+
get wrapper() {
|
|
74
|
+
const ast = parse(this.combinedJs, {
|
|
75
|
+
ecmaVersion: 'latest',
|
|
76
|
+
sourceType: 'script',
|
|
77
|
+
allowReserved: true
|
|
78
|
+
});
|
|
79
|
+
const getBody = (ast, type) => {
|
|
80
|
+
return (ast.type === 'Program' ? ast.body.filter(f => f.type === type) : [])
|
|
81
|
+
}
|
|
82
|
+
const makeExports = (ast, type, accessor) => getBody(ast, type).map((f) => accessor(f))
|
|
83
|
+
const functions = makeExports(ast, 'FunctionDeclaration', (f) => f.id.name)
|
|
84
|
+
const variables = makeExports(ast, 'VariableDeclaration', (f) => f.declarations[0].id.name)
|
|
85
|
+
const classes = makeExports(ast, 'ClassDeclaration', (f) => f.id.name)
|
|
86
|
+
const exports = [...functions, ...variables, ...classes];
|
|
87
|
+
return `${this.combinedJs};\nreturn { ${exports.join(', ')} };`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
inject() {
|
|
91
|
+
if (!this.wrapper) {
|
|
92
|
+
throw new Error('wrapper not loaded');
|
|
93
|
+
}
|
|
94
|
+
globalThis[this.userSymbol] = (new Function(this.wrapper))()
|
|
95
|
+
return this
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get manifest() {
|
|
99
|
+
if (!this.__manifest) {
|
|
100
|
+
const t = this.libContent?.files?.find((f) => f.type === 'json' && f.name === 'appsscript')
|
|
101
|
+
if (!t) {
|
|
102
|
+
throw new Error(`manifest not found in library ${this.libraryId}`)
|
|
103
|
+
}
|
|
104
|
+
this.__manifest = JSON.parse(t.source)
|
|
105
|
+
}
|
|
106
|
+
return this.__manifest
|
|
107
|
+
}
|
|
108
|
+
get serverJs() {
|
|
109
|
+
return this.libContent?.files?.filter((f) => f.type === 'server_js')
|
|
110
|
+
}
|
|
111
|
+
get libraries() {
|
|
112
|
+
return this.manifest?.dependencies?.libraries || null;
|
|
113
|
+
}
|
|
114
|
+
toString() {
|
|
115
|
+
return 'Library';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -348,7 +348,7 @@ class FakeBehavior {
|
|
|
348
348
|
return id
|
|
349
349
|
}
|
|
350
350
|
isAccessible(id, serviceName, accessType = 'read') {
|
|
351
|
-
if (!is.nonEmptyString(id)) {
|
|
351
|
+
if (this.sandboxMode && !is.nonEmptyString(id)) {
|
|
352
352
|
throw new Error(`Invalid sandbox id parameter (${id}) - must be a non-empty string`);
|
|
353
353
|
}
|
|
354
354
|
|
package/src/support/sxdrive.js
CHANGED
|
@@ -139,25 +139,17 @@ export const sxStreamUpMedia = async (Auth, { resource, bytes, fields, method, m
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* @param {string} p.id file id
|
|
147
|
-
* @return {SxResult} from the api
|
|
148
|
-
*/
|
|
149
|
-
export const sxDriveMedia = async (Auth, { id }) => {
|
|
150
|
-
|
|
142
|
+
const sxStreamer = async ({
|
|
143
|
+
params,
|
|
144
|
+
options = {},
|
|
145
|
+
method = 'get' }) => {
|
|
151
146
|
// this is the node drive service
|
|
152
147
|
const drive = getDriveApiClient();
|
|
153
|
-
const streamed = await drive.files
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}, {
|
|
157
|
-
responseType: 'stream'
|
|
148
|
+
const streamed = await drive.files[method](params, {
|
|
149
|
+
responseType: 'stream',
|
|
150
|
+
...options
|
|
158
151
|
})
|
|
159
152
|
const response = responseSyncify(streamed)
|
|
160
|
-
|
|
161
153
|
if (response.status === 200) {
|
|
162
154
|
const buf = await getStreamAsBuffer(streamed.data)
|
|
163
155
|
const data = Array.from(buf)
|
|
@@ -172,9 +164,37 @@ export const sxDriveMedia = async (Auth, { id }) => {
|
|
|
172
164
|
response
|
|
173
165
|
}
|
|
174
166
|
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* sync a call to export data from drive
|
|
170
|
+
* @param {object} p pargs
|
|
171
|
+
* @param {string} p.id file id
|
|
172
|
+
* @return {SxResult} from the api
|
|
173
|
+
*/
|
|
174
|
+
export const sxDriveExport = async (_, { id: fileId, mimeType }) => {
|
|
175
|
+
|
|
176
|
+
return sxStreamer({ params: {
|
|
177
|
+
fileId,
|
|
178
|
+
mimeType
|
|
179
|
+
}, method: 'export' })
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* sync a call to download data from drive
|
|
184
|
+
* @param {object} p pargs
|
|
185
|
+
* @param {string} p.id file id
|
|
186
|
+
* @return {SxResult} from the api
|
|
187
|
+
*/
|
|
188
|
+
export const sxDriveMedia = async (_, { id: fileId }) => {
|
|
189
|
+
|
|
190
|
+
return sxStreamer({params: {
|
|
191
|
+
fileId,
|
|
192
|
+
alt: 'media'
|
|
193
|
+
}, method: 'get' })
|
|
175
194
|
|
|
176
195
|
}
|
|
177
196
|
|
|
197
|
+
|
|
178
198
|
export const sxDriveGet = (Auth, { id, params, options }) => {
|
|
179
199
|
return sxDrive(Auth, {
|
|
180
200
|
prop: "files",
|
package/src/support/syncit.js
CHANGED
|
@@ -333,6 +333,23 @@ const fxDriveMedia = ({ id }) => {
|
|
|
333
333
|
id,
|
|
334
334
|
});
|
|
335
335
|
};
|
|
336
|
+
/**
|
|
337
|
+
* sync a call to Drive api to stream a download
|
|
338
|
+
* @param {object} p pargs
|
|
339
|
+
* @param {string} p.prop the prop of drive eg 'files' for drive.files
|
|
340
|
+
* @param {string} p.method the method of drive eg 'list' for drive.files.list
|
|
341
|
+
* @param {object} p.params the params to add to the request
|
|
342
|
+
* @return {DriveResponse} from the drive api
|
|
343
|
+
*/
|
|
344
|
+
const fxDriveExport = ({ id, mimeType , options ={alt: 'media'}}) => {
|
|
345
|
+
// see issue https://issuetracker.google.com/issues/468534237
|
|
346
|
+
// live apps script failes without this alt option
|
|
347
|
+
return callSync("sxDriveExport", {
|
|
348
|
+
id,
|
|
349
|
+
mimeType,
|
|
350
|
+
options
|
|
351
|
+
});
|
|
352
|
+
};
|
|
336
353
|
|
|
337
354
|
/**
|
|
338
355
|
* a sync version of fetching
|
|
@@ -399,5 +416,5 @@ export const Syncit = {
|
|
|
399
416
|
fxDocs,
|
|
400
417
|
fxForms,
|
|
401
418
|
fxGmail,
|
|
402
|
-
|
|
403
|
-
}
|
|
419
|
+
fxDriveExport
|
|
420
|
+
}
|
package/testlib.sh
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
gas-fakes -l bmFiddler@13EWG4-lPrEf34itxQhAQ7b9JEbmCBfO8uE4Mhr99CHi3Pw65oxXtq-rU \
|
|
2
|
+
-s "const sheet=SpreadsheetApp.openById('1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ').getSheets()[0];const fiddler = new bmFiddler.Fiddler(sheet);console.log (fiddler.getData().slice(0, 5));"
|