@mcpher/gas-fakes 2.0.8 → 2.0.10

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
@@ -129,15 +129,13 @@ It contains a cloud logging query that will display any logging done in this ses
129
129
 
130
130
  You will have used the gas-fakes init command to create a .env file, containing the LOG_DESTINATION setting. You can change any of the settings in the .env file manually if you want to.
131
131
 
132
- If you want to set an initial LOG_DESTINATION using that .env file, you have to let gas-fakes know where to find it. Let's assume it's in the same folder as your main script.
132
+ If you want to set an initial LOG_DESTINATION using that .env file, you have to let gas-fakes know where to find it. Let's assume it's in the same folder as your main script.
133
133
  ```env
134
- node --env-file=.env yourapp.js
135
- ```
136
- Some developers prefer to use dotenv to set the path of the .env file
137
- ```javascript
138
- import dotenv from 'dotenv'
139
- dotenv.config({ path: '/custom/path/to/.env' })
134
+ node yourapp.js
135
+ # or if your .env is somewhere else
136
+ node --env-file pathtoenv yourapp.js
140
137
  ```
138
+
141
139
  Alternatively, instead of putting it in an env file, you can export it in your shell environment.
142
140
  ```sh
143
141
  export LOG_DESTINATION="BOTH"
@@ -170,7 +168,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
170
168
  - [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
171
169
  - [readme](README.md)
172
170
  - [gas fakes cli](gas-fakes-cli.md)
173
- - [running gas-fakes on google cloud run](cloud-run.md)
171
+ - [running gas-fakes on google cloud run](https://github.com/brucemcpherson/gas-fakes-containers)
172
+ - [running gas-fakes on google kubernetes engine](https://github.com/brucemcpherson/gas-fakes-containers)
174
173
  - [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
175
174
  - [Inside the volatile world of a Google Document](https://ramblings.mcpher.com/inside-the-volatile-world-of-a-google-document/)
176
175
  - [Apps Script Services on Node – using apps script libraries](https://ramblings.mcpher.com/apps-script-services-on-node-using-apps-script-libraries/)
package/package.json CHANGED
@@ -13,16 +13,16 @@
13
13
  "dotenv": "^17.3.1",
14
14
  "fast-xml-parser": "^5.3.6",
15
15
  "get-stream": "^9.0.1",
16
- "googleapis": "^170.1.0",
16
+ "googleapis": "^171.4.0",
17
17
  "got": "^14.6.6",
18
- "into-stream": "^8.0.1",
18
+ "into-stream": "^9.1.0",
19
19
  "keyv": "^5.6.0",
20
20
  "keyv-file": "^5.3.3",
21
21
  "mime": "^4.1.0",
22
22
  "prompts": "^2.4.2",
23
23
  "sleep-synchronously": "^2.0.0",
24
24
  "unzipper": "^0.12.3",
25
- "zod": "^3.25.76"
25
+ "zod": "^4.3.6"
26
26
  },
27
27
  "type": "module",
28
28
  "scripts": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "name": "@mcpher/gas-fakes",
35
35
  "author": "bruce mcpherson",
36
- "version": "2.0.8",
36
+ "version": "2.0.10",
37
37
  "license": "MIT",
38
38
  "main": "main.js",
39
39
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
package/src/cli/app.js CHANGED
@@ -82,9 +82,17 @@ export async function main() {
82
82
  return;
83
83
  }
84
84
 
85
- const envPath = path.resolve(process.cwd(), env);
86
- console.log(`...using env file in ${envPath}`);
87
- dotenv.config({ path: envPath, quiet: true });
85
+ // Load environment variables.
86
+ // If --env-file is used, we skip the default .env to avoid conflicts,
87
+ // but if the user explicitly provided -e, we use that.
88
+ const hasEnvFileFlag = process.execArgv.some(arg => arg.startsWith('--env-file'));
89
+ const isDefaultEnv = env === "./.env";
90
+
91
+ if (!hasEnvFileFlag || !isDefaultEnv) {
92
+ const envPath = path.resolve(process.cwd(), env);
93
+ console.log(`...using env file in ${envPath}`);
94
+ dotenv.config({ path: envPath, quiet: true });
95
+ }
88
96
 
89
97
  const settingsPath = path.resolve(process.cwd(), gfsettings);
90
98
  console.log(`...using gasfakes settings file in ${settingsPath}`);
package/src/index.js CHANGED
@@ -1,6 +1,5 @@
1
-
1
+ import './support/env-loader.js';
2
2
  import './services/scriptapp/app.js'
3
-
4
3
  import './services/driveapp/app.js'
5
4
  import './services/logger/app.js'
6
5
  import './services/urlfetchapp/app.js'
@@ -60,7 +60,7 @@ export class FakeTextFinder {
60
60
  }
61
61
  return (
62
62
  this.searchResults[
63
- this.count == 0 ? this.searchResults.length - 1 : --this.count
63
+ this.count == 0 ? this.searchResults.length - 1 : --this.count
64
64
  ] || null
65
65
  );
66
66
  }
@@ -189,6 +189,9 @@ export class FakeTextFinder {
189
189
  target = c.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
190
190
  }
191
191
  if (!matchCase) {
192
+ if (typeof c !== 'string') {
193
+ c = String(c);
194
+ }
192
195
  target = c.toLowerCase();
193
196
  this.text = this.text.toLowerCase();
194
197
  }
@@ -2,7 +2,6 @@ import { GoogleAuth, JWT, Impersonated, OAuth2Client } from "google-auth-library
2
2
  import is from "@sindresorhus/is";
3
3
  import { createHash } from "node:crypto";
4
4
  import { syncLog, syncError } from "./workersync/synclogger.js";
5
- import fs from "node:fs";
6
5
 
7
6
  const _authScopes = new Set([]);
8
7
 
@@ -225,6 +224,9 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
225
224
  this._expiresAt = 0
226
225
  this.credentials = null
227
226
  }
227
+ _authClient.refreshAccessToken = function () {
228
+ return this.getAccessToken()
229
+ }
228
230
 
229
231
  mayLog(`...using Domain-Wide Delegation for user: ${userEmail}`)
230
232
 
@@ -0,0 +1,34 @@
1
+ import dotenv from 'dotenv';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+
5
+ /**
6
+ * Conditionally loads environment variables from a .env file.
7
+ *
8
+ * If Node.js was started with the --env-file flag, this loader does nothing
9
+ * to allow the native Node.js environment file handling to take precedence.
10
+ *
11
+ * Otherwise, it attempts to load a .env file from the directory where the
12
+ * main script is located. If not found, it falls back to the default
13
+ * dotenv behavior (searching the current working directory).
14
+ */
15
+
16
+ // Check if node was run with --env-file
17
+ const hasEnvFileFlag = process.execArgv.some(arg => arg.startsWith('--env-file'));
18
+
19
+ if (!hasEnvFileFlag) {
20
+ const mainScript = process.argv[1];
21
+ if (mainScript) {
22
+ // Try to find .env in the same directory as the main entry point script
23
+ const envPath = join(dirname(mainScript), '.env');
24
+ if (existsSync(envPath)) {
25
+ dotenv.config({ path: envPath });
26
+ } else {
27
+ // Fallback to default dotenv behavior (CWD)
28
+ dotenv.config();
29
+ }
30
+ } else {
31
+ // Fallback if mainScript is not available (e.g. REPL)
32
+ dotenv.config();
33
+ }
34
+ }
@@ -77,9 +77,13 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
77
77
  // now update all that if anything has changed
78
78
  const strSet = JSON.stringify(settings, null, 2)
79
79
  if (JSON.stringify(_settings, null, 2) !== strSet) {
80
+ try {
80
81
  await mkdir(settingsDir, { recursive: true })
81
82
  syncLog(`...writing to ${settingsPath}`);
82
83
  writeFile(settingsPath, strSet, { flag: 'w' })
84
+ } catch (err) {
85
+ syncWarn(`...unable to write settings file (normal in readonly filesystem) - skipping ${settingsPath}: ${err}`)
86
+ }
83
87
  }
84
88
 
85
89
  // get the required scopes and set them
@@ -17,7 +17,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
17
17
  */
18
18
  export const sxRetry = async (Auth, tag, func, options = {}) => {
19
19
  const {
20
- maxRetries = 7,
20
+ maxRetries = 9,
21
21
  initialDelay = 1777,
22
22
  extraRetryCheck = () => false,
23
23
  skipLog = () => false
@@ -63,16 +63,20 @@ export const sxRetry = async (Auth, tag, func, options = {}) => {
63
63
  retryReason = 'Rate Limit';
64
64
  }
65
65
 
66
+ if (!retryReason && error?.message?.toLowerCase().includes('no refresh token')) {
67
+ retryReason = 'No Refresh Token';
68
+ }
69
+
66
70
  const isRetryable = !!retryReason || extraRetryCheck(error, response);
67
71
  if (isRetryable && !retryReason) retryReason = 'Extra Check';
68
72
 
69
73
  if (isRetryable && i < maxRetries - 1) {
70
- const isAuthError = error?.code === 401 || status === 401;
74
+ const isAuthError = error?.code === 401 || status === 401 || retryReason === 'No Refresh Token';
71
75
  if (isAuthError) {
72
76
  // Only retry auth error once
73
77
  if (i > 0) break;
74
78
  Auth.invalidateToken();
75
- syncWarn(`Authentication error (401) on ${tag}. Invalidated token and retrying immediately...`);
79
+ syncWarn(`Authentication error (${status || retryReason}) on ${tag}. Invalidated token and retrying immediately...`);
76
80
  } else {
77
81
  const jitter = Math.floor(Math.random() * 1000);
78
82
  const totalDelay = delay + jitter;
@@ -1,4 +1,5 @@
1
1
  import { Auth } from './auth.js'
2
+ import { sxRetry } from './sxretry.js'
2
3
 
3
4
  /**
4
5
  * fetch effective user access token
@@ -12,4 +13,23 @@ export const sxGetAccessTokenInfo = async () => {
12
13
  }
13
14
  export const sxGetSourceAccessTokenInfo = async () => {
14
15
  return await Auth.getSourceAccessTokenInfo()
15
- }
16
+ }
17
+
18
+ /**
19
+ * For testing sxRetry logic
20
+ */
21
+ export const sxTestRetry = async (Auth, { errorMessage }) => {
22
+ let callCount = 0;
23
+ const mockFunc = async () => {
24
+ callCount++;
25
+ if (callCount === 1) {
26
+ throw new Error(errorMessage);
27
+ }
28
+ return {
29
+ status: 200,
30
+ data: { success: true, callCount }
31
+ };
32
+ };
33
+
34
+ return await sxRetry(Auth, 'TestRetry', mockFunc);
35
+ };
@@ -381,6 +381,10 @@ const fxGetSourceAccessTokenInfo = () => {
381
381
  return callSync("sxGetSourceAccessTokenInfo");
382
382
  };
383
383
 
384
+ const fxTestRetry = (errorMessage) => {
385
+ return callSync("sxTestRetry", { errorMessage });
386
+ };
387
+
384
388
  const fxSheets = (args) =>
385
389
  fxGeneric({
386
390
  ...args,
@@ -447,5 +451,6 @@ export const Syncit = {
447
451
  fxDriveExport,
448
452
  fxGetAccessToken,
449
453
  fxGetAccessTokenInfo,
450
- fxGetSourceAccessTokenInfo
454
+ fxGetSourceAccessTokenInfo,
455
+ fxTestRetry
451
456
  }
package/debug_form.js DELETED
@@ -1,43 +0,0 @@
1
- import './main.js';
2
- import { Syncit } from './src/support/syncit.js';
3
-
4
- async function debug() {
5
- Syncit.fxInit();
6
-
7
- try {
8
- const resource = Forms.Form.create({
9
- info: {
10
- title: "Debug Form with Items"
11
- }
12
- });
13
- const formId = resource.formId;
14
-
15
- // Add a text item
16
- const updateRequest = {
17
- requests: [{
18
- createItem: {
19
- item: {
20
- title: "Question 1",
21
- questionItem: {
22
- question: {
23
- textQuestion: { paragraph: false }
24
- }
25
- }
26
- },
27
- location: { index: 0 }
28
- }
29
- }]
30
- };
31
-
32
- const updateResponse = Forms.Form.batchUpdate(updateRequest, formId);
33
- console.log("Update Response:", JSON.stringify(updateResponse, null, 2));
34
-
35
- const fullForm = Forms.Form.get(formId);
36
- console.log("Full Form Resource:", JSON.stringify(fullForm, null, 2));
37
-
38
- } catch (e) {
39
- console.error("Error:", e);
40
- }
41
- }
42
-
43
- debug();