@mcpher/gas-fakes 1.2.4 → 1.2.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
@@ -20,6 +20,16 @@ Collaborators should fork the repo and use the local versions of these files - s
20
20
 
21
21
  Just as on Apps Script, everything is executed synchronously so you don't need to bother with handling Promises/async/await. Just write normal Apps Script code. Usually you would have an associated App Script project if that's your eventual target, but it's not essential that you do. You can get started right away on Node.
22
22
 
23
+ # gas-fakes-cli
24
+
25
+ Now you can run apps script code directly from your console - for example
26
+
27
+ ```bash
28
+ gas-fakes -s "const files=DriveApp.getRootFolder().searchFiles('title contains \"Untitled\"');while (files.hasNext()) {console.log(files.next().getName())};"
29
+ ```
30
+
31
+ For details see [gas fakes cli](gas-fakes-cli.md)
32
+
23
33
  ### Settings
24
34
 
25
35
  The optional `gasfakes.json` file holds various location and behavior parameters for your local Node environment. It is not required on GAS, as you can't change anything over there. If you don't provide this file, `gas-fakes` will create one for you with sensible defaults.
@@ -134,12 +144,12 @@ There are a couple of syntactical differences between Node and Apps Script. Not
134
144
  // this required on Node but not on Apps Script
135
145
  if (ScriptApp.isFake) testFakes()
136
146
  ````
137
- For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite.
147
+ For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](pull-test-push.md)
148
+
138
149
 
139
150
  ## Help
140
151
 
141
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.
142
-
143
153
  ## <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
144
154
 
145
155
  - [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
@@ -159,3 +169,4 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
159
169
  - [named range identity](named-range-identity.md)
160
170
  - [adc and restricted scopes](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
161
171
  - [push test pull](pull-test-push.md)
172
+ - [gas fakes cli](gas-fakes-cli.md)
package/gas-fakes.js ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * cli for gas-fakes
5
+ * v0.0.1
6
+ */
7
+ import fs from "fs";
8
+ import { Command } from "commander";
9
+ import dotenv from 'dotenv'
10
+
11
+ const version = "0.0.2";
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("gas-fakes")
17
+ .description("CLI tool for gas-fakes")
18
+ .version(version, "-v, --version", "display the current version");
19
+
20
+
21
+ program
22
+ .description("Execute Google Apps Script using gas-fakes.")
23
+ .option(
24
+ "-f, --filename <string>",
25
+ "filename of the file including Google Apps Script. When this is used, the option --script is ignored."
26
+ )
27
+ .option(
28
+ "-e, --env <path>",
29
+ "provide path to your .env file for special options."
30
+ )
31
+ .option(
32
+ "-g, --gfsettings <path>",
33
+ "provide path to your gasfakes.json file for script options."
34
+ )
35
+ .option(
36
+ "-s, --script <string>",
37
+ "provide Google Apps Script as a string. When this is used, the option --filename is ignored."
38
+ )
39
+ .option("-x, --sandbox", "run Google Apps Script in a sandbox.")
40
+ .option(
41
+ "-w, --whitelist <string>",
42
+ "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."
43
+ )
44
+ .option(
45
+ "-j, --json <string>",
46
+ `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.`
47
+ )
48
+ .option(
49
+ "-d, --display",
50
+ `display the created script for executing with gas-fakes. Default is false.`,
51
+ false
52
+ )
53
+ .action((options) => {
54
+ if (Object.keys(options).length == 0) {
55
+ program.help();
56
+ } else {
57
+ const { filename, script, sandbox, whitelist, json, display, env , gfsettings} = options;
58
+ const obj = { sandbox: !!sandbox, display };
59
+ if (!filename && !script) {
60
+ console.error(
61
+ "error: Provide the filename or the script of Google Apps Script."
62
+ );
63
+ process.exit();
64
+ }
65
+ if (gfsettings) {
66
+ obj.gfSettings= gfsettings
67
+ }
68
+ if (env) {
69
+ dotenv.config({ path: env })
70
+ }
71
+ if (filename) {
72
+ obj.filename = filename;
73
+ }
74
+ if (script) {
75
+ obj.script = script;
76
+ }
77
+
78
+ // for sandbox
79
+ if (whitelist) {
80
+ const ar = whitelist.split(",").map((e) => e.trim());
81
+ if (ar.length > 0) {
82
+ obj.whitelistItems = ar;
83
+ }
84
+ }
85
+ if (json) {
86
+ try {
87
+ const temp = JSON.parse(json);
88
+ obj.json_sandbox = temp;
89
+ } catch (err) {
90
+ console.error("error: Invalid JSON.");
91
+ process.exit();
92
+ }
93
+ }
94
+ loadScript(obj);
95
+ }
96
+ });
97
+
98
+ program.showHelpAfterError("(add --help for additional information)");
99
+ program.parse();
100
+
101
+ function __getImportScript(o) {
102
+ const { scriptText, sandbox, whitelistItems, json_sandbox, gfSettings } = o;
103
+ if (scriptText.trim() == "") {
104
+ console.error("error: Google Apps Script was not found.");
105
+ process.exit();
106
+ }
107
+ const gasScriptAr = [];
108
+ if (json_sandbox) {
109
+ gasScriptAr.push(
110
+ `const behavior = ScriptApp.__behavior;`,
111
+ `behavior.sandboxMode = true;`,
112
+ `behavior.strictSandbox = true;`
113
+ );
114
+ const { whitelistItems, whitelistServices, blacklistServices } =
115
+ json_sandbox;
116
+ if (whitelistServices && whitelistServices.length > 0) {
117
+ const bl = whitelistServices.flatMap(({ className, methodNames }, i) => {
118
+ if (!className) {
119
+ console.error(
120
+ "error: Class name was not found in whitelistServices."
121
+ );
122
+ process.exit();
123
+ }
124
+ const k = `s${i + 1}`;
125
+ const temp = [`const ${k} = behavior.sandboxService.${className};`];
126
+ if (methodNames && methodNames.length > 0) {
127
+ temp.push(
128
+ `${k}.setMethodWhitelist([${methodNames.map((e) => `"${e}"`)}]);`
129
+ );
130
+ }
131
+ return temp;
132
+ });
133
+ gasScriptAr.push(...bl);
134
+ }
135
+ if (blacklistServices && blacklistServices.length > 0) {
136
+ const bl = blacklistServices.map(
137
+ (e) => `behavior.sandboxService.${e}.enabled = false;`
138
+ );
139
+ gasScriptAr.push(bl);
140
+ }
141
+ if (whitelistItems && whitelistItems.length > 0) {
142
+ const wl = whitelistItems
143
+ .map(({ itemId = "", read = true, write = false, trash = false }) => {
144
+ if (!itemId) {
145
+ console.error("error: itemId was not found in whitelistItems.");
146
+ process.exit();
147
+ }
148
+ return `behavior.newIdWhitelistItem("${itemId}").setRead(${read}).setWrite(${write}).setTrash(${trash})`;
149
+ })
150
+ .join(",");
151
+ gasScriptAr.push(`behavior.setIdWhitelist([${wl}]);`);
152
+ }
153
+ gasScriptAr.push(`\n\n${scriptText}\n\n`, `ScriptApp.__behavior.trash();`);
154
+ } else {
155
+ if (sandbox && (!whitelistItems || whitelistItems.length === 0)) {
156
+ gasScriptAr.push(
157
+ sandbox ? `ScriptApp.__behavior.sandBoxMode = true;` : "",
158
+ `\n\n${scriptText}\n\n`,
159
+ sandbox ? `ScriptApp.__behavior.trash();` : ""
160
+ );
161
+ } else if (whitelistItems && whitelistItems.length > 0) {
162
+ const wl = whitelistItems
163
+ .map((id) => `behavior.newIdWhitelistItem("${id}").setWrite(true)`)
164
+ .join(",");
165
+ gasScriptAr.push(
166
+ `const behavior = ScriptApp.__behavior;`,
167
+ `behavior.sandboxMode = true;`,
168
+ `behavior.strictSandbox = true;`,
169
+ `behavior.setIdWhitelist([${wl}]);`,
170
+ `\n\n${scriptText}\n\n`,
171
+ `ScriptApp.__behavior.trash();`
172
+ );
173
+ } else {
174
+ gasScriptAr.push(scriptText);
175
+ }
176
+ }
177
+ const importScriptAr = [
178
+ `async function runGas() {`,
179
+ // Pass the settings path to the init function if it's provided
180
+ gfSettings
181
+ ? `const settingsPath = "${gfSettings}";`
182
+ : `const settingsPath = undefined;`,
183
+ `await import("./main.js");`, // This will trigger the fxInit call
184
+ ...gasScriptAr,
185
+ `};`,
186
+ ``,
187
+ `runGas();`,
188
+ ];
189
+ return {
190
+ mainScript: importScriptAr.join("\n"),
191
+ gasScript: gasScriptAr.join("\n"),
192
+ };
193
+ }
194
+
195
+ async function loadScript(o) {
196
+ const { filename, script, display } = o;
197
+
198
+ const scriptText = filename ? fs.readFileSync(filename, "utf8") : script;
199
+ const { mainScript, gasScript } = __getImportScript({ scriptText, ...o });
200
+ if (display) {
201
+ console.log(`--- script ---`);
202
+ console.log(gasScript);
203
+ console.log(`--- /script ---`);
204
+ }
205
+ const gasFunc = new Function(mainScript);
206
+ // The script needs access to the settings path variable we just created
207
+ Object.defineProperty(globalThis, "settingsPath", {
208
+ value: o.gfSettings,
209
+ writable: true,
210
+ configurable: true,
211
+ });
212
+ gasFunc();
213
+ }
package/gasfakes.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest": "./appsscript.json",
3
3
  "clasp": "./.clasp.json",
4
- "scriptId": "80bcefe1-604f-4495-a82f-d4d8fab2b3df",
4
+ "scriptId": "7bcfb7cc-6200-4a7d-9f33-e1a3f3d19529",
5
5
  "documentId": null,
6
6
  "cache": "/tmp/gas-fakes/cache",
7
7
  "properties": "/tmp/gas-fakes/properties"
package/package.json CHANGED
@@ -7,6 +7,8 @@
7
7
  "@mcpher/gas-flex-cache": "^1.1.2",
8
8
  "@sindresorhus/is": "^7.0.1",
9
9
  "archiver": "^7.0.1",
10
+ "commander": "^14.0.1",
11
+ "dotenv": "^17.2.3",
10
12
  "exceljs": "^4.4.0",
11
13
  "fast-json-stable-stringify": "^2.1.0",
12
14
  "get-stream": "^9.0.1",
@@ -73,10 +75,13 @@
73
75
  },
74
76
  "name": "@mcpher/gas-fakes",
75
77
  "author": "bruce mcpherson",
76
- "version": "1.2.4",
78
+ "version": "1.2.6",
77
79
  "license": "MIT",
78
80
  "main": "main.js",
79
81
  "description": "A proof of concept implementation of Apps Script Environment on Node",
80
82
  "repository": "github:brucemcpherson/gas-fakes",
81
- "homepage": "https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/"
83
+ "homepage": "https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/",
84
+ "bin": {
85
+ "gas-fakes": "gas-fakes.js"
86
+ }
82
87
  }
@@ -32,6 +32,9 @@ export const getFilesIterator = ({
32
32
  if (qob) {
33
33
  const regex = /(^|\s)title(\s*=\s*|\s+contains\s+|\s*!=\s*|\s*>\s*|\s*<\s*)/gi;
34
34
  qob = qob.map(f => {
35
+ if (!is.string(f)) {
36
+ throw new Error (`invalid parameters ${JSON.stringify(qob)}`)
37
+ }
35
38
  return f.replace(regex, (match, p1, p2) => {
36
39
  // p1 is the start-of-string or whitespace before 'title'
37
40
  // p2 is the operator and surrounding whitespace
@@ -39,7 +42,7 @@ export const getFilesIterator = ({
39
42
  });
40
43
  })
41
44
  }
42
-
45
+ //qob = ["name contains 'Untitled' and '0AN2ExLh4POiZUk9PVA' in parents and mimeType != 'application/vnd.google-apps.folder'"]
43
46
  /**
44
47
  * this generator will get chunks of matching files from the drive api
45
48
  * and yield them 1 by 1 and handle paging if required
@@ -142,6 +145,7 @@ const fileLister = ({
142
145
  }) => {
143
146
  // enhance any already supplied query params
144
147
  qob = Utils.arrify(qob) || []
148
+ qob = [...qob]
145
149
  if (parentId) {
146
150
  ScriptApp.__behavior.isAccessible(parentId) // will throw if not accessible
147
151
  qob.push(`'${parentId}' in parents`)
@@ -9,7 +9,7 @@ import got from 'got';
9
9
  import { Auth } from './auth.js';
10
10
  import { syncError, syncLog } from './workersync/synclogger.js';
11
11
  import { homedir } from 'os';
12
- import {access, readFile, writeFile, mkdir } from 'fs/promises';
12
+ import { access, readFile, writeFile, mkdir } from 'fs/promises';
13
13
  import path from 'path'
14
14
 
15
15
 
@@ -47,10 +47,11 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, mainDir, c
47
47
  }
48
48
 
49
49
  // files are relative to this main path
50
+
50
51
  const settingsFile = path.resolve(mainDir, settingsPath)
51
52
  const settingsDir = path.dirname(settingsFile)
52
53
 
53
-
54
+ // syncLog (JSON.stringify({mainDir,settingsPath,settingsDir,settingsFile}))
54
55
  // get the setting file if it exists
55
56
  const _settings = await getIfExists(path.resolve(mainDir, settingsFile))
56
57
  const settings = { ..._settings }
@@ -91,12 +92,27 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, mainDir, c
91
92
  // Initialize auth. This is async and will discover the project ID.
92
93
  const auth = await Auth.setAuth(scopes);
93
94
  const projectId = Auth.getProjectId();
94
- const accessToken = await auth.getAccessToken()
95
+ let accessToken = null
96
+
97
+ // need to handle an expired refresh token
98
+ try {
99
+ accessToken = await auth.getAccessToken()
100
+ } catch (error) {
101
+
102
+ }
95
103
  let tokenInfo = null
96
104
  try {
97
105
  tokenInfo = await got(`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${accessToken}`).json()
98
106
  } catch (err) {
99
- syncError (`failed to get access token info`)
107
+ syncError(`Application default credentials needs attention`)
108
+ if (error.code === 400 && error.message.includes('invalid_grant')) {
109
+ // Log a specific, actionable error for the developer/operator
110
+ syncError("ADC 'invalid_grant' Error: The underlying Application Default Credentials have expired or been revoked.");
111
+ syncError("Helpful note: in your admin console under security/access amd data control there are a couple of settings to fiddle with token life")
112
+ syncError("Use your settaccount.sh to reauthenticate");
113
+ throw new Error ('failed to get access token info : Use your settaccount.sh to reauthenticate')
114
+ }
115
+ throw error; // Re-throw any other errors
100
116
  }
101
117
 
102
118
  /// these all jst exist in this sub process so we need to send them back to parent process
@@ -22,7 +22,7 @@ import { callSync } from "./workersync/synchronizer.js";
22
22
 
23
23
  const manifestDefaultPath = "./appsscript.json";
24
24
  const claspDefaultPath = "./.clasp.json";
25
- const settingsDefaultPath = "./gasfakes.json";
25
+ const settingsDefaultPath = "./gasfakes.json" || process.env.GF_SETTINGS_PATH;
26
26
  const propertiesDefaultPath = "/tmp/gas-fakes/properties";
27
27
  const cacheDefaultPath = "/tmp/gas-fakes/cache";
28
28
  // note that functions like Sheets.newGridRange() etc create objects that contain get and set functions
@@ -262,7 +262,7 @@ const fxUnzipper = ({ blob }) => {
262
262
  const fxInit = ({
263
263
  manifestPath = manifestDefaultPath,
264
264
  claspPath = claspDefaultPath,
265
- settingsPath = settingsDefaultPath,
265
+ settingsPath = globalThis.settingsPath || settingsDefaultPath,
266
266
  cachePath = cacheDefaultPath,
267
267
  propertiesPath = propertiesDefaultPath,
268
268
  } = {}) => {