@mcpher/gas-fakes 1.0.21 → 1.0.23

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 CHANGED
@@ -328,7 +328,6 @@ const getParentsIterator = ({
328
328
  - [oddities](oddities.md) - a collection of oddities uncovered during this project
329
329
  - [gemini](gemini.md) - some reflections and experiences on using gemini to help code large projects
330
330
  - [named colors](named-colors.md) - colors supported by Apps Script
331
- - [setup env](setup-env.md) - ([credit Eric Shapiro] - additional info on contents of .env file
332
331
  - [this file](README.md)
333
332
  - [named colors](named-colors.md)
334
333
  - [sandbox](sandbox.md)
package/README.md CHANGED
@@ -120,7 +120,7 @@ When logging to the cloud, entries are sent to the `gas-fakes/console_logs` log
120
120
 
121
121
  #### Dynamic Control
122
122
 
123
- You can change the logging destination at any time during runtime using the `Logger.__setLogDestination()` method. This is especially useful for testing or for applications that need to change their logging behavior dynamically.
123
+ You can change the logging destination at any time during runtime by setting the `Logger.__logDestination` property. This is especially useful for testing or for applications that need to change their logging behavior dynamically.
124
124
 
125
125
  The method accepts one of the following string values: `'CONSOLE'`, `'CLOUD'`, `'BOTH'`, or `'NONE'`.
126
126
 
@@ -137,13 +137,47 @@ To change the destination during runtime in your script:
137
137
  Logger.log('This goes to console and cloud');
138
138
 
139
139
  // Switch to only logging to the console
140
- Logger.__setLogDestination('CONSOLE');
140
+ Logger.__logDestination='CONSOLE';
141
141
  Logger.log('This now only goes to the console');
142
142
 
143
143
  // Disable logging completely
144
- Logger.__setLogDestination('NONE');
144
+ Logger.__logDestination='NONE';
145
145
  Logger.log('This goes nowhere');
146
146
  ```
147
+
148
+ #### Link to Cloud log for this run
149
+
150
+ If you have used Logging to cloud, you can get a link to the log data like this.
151
+
152
+ ```javascript
153
+ console.log ('....example cloud log link for this session',Logger.__cloudLogLink)
154
+ ```
155
+
156
+ It contains a cloud logging query that will display any logging done in this session - the filter is based on the scriptId (from gasfakes.json), the projectId and userId (from Auth), as well as the start and end time of the session.
157
+
158
+ #### A note on .env location
159
+
160
+ Optionally you can set the initial Logging environment in your .env file. By default it assumes you want to log to the console only.
161
+ 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.
162
+ ```env
163
+ node --env-file=.env yourapp.js
164
+ ```
165
+ Some developers prefer to use [dotenv](https://www.npmjs.com/package/dotenv) to set the path of the .env file
166
+ ```javascript
167
+ import dotenv from 'dotenv'
168
+ dotenv.config({ path: '/custom/path/to/.env' })
169
+ ```
170
+ Alternatively, instead of putting it in an env file, you can export it in your shell environment.
171
+ ```sh
172
+ export LOG_DESTINATION="BOTH"
173
+ ```
174
+ Finally, another approach is to set it dynamically at the beginning of your app.
175
+ ```javascript
176
+ Logger.__logDestination="BOTH"
177
+ ```
178
+
179
+ Do whichever one suits you best.
180
+
147
181
  ### Pushing files to GAS
148
182
 
149
183
  There are a couple of syntactical differences between Node and Apps Script. Not in the body of the code but in how the IDE executes. The 2 main ones are
@@ -173,7 +207,6 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
173
207
  - [oddities](oddities.md) - a collection of oddities uncovered during this project
174
208
  - [gemini](gemini.md) - some reflections and experiences on using gemini to help code large projects
175
209
  - [named colors](named-colors.md) - colors supported by Apps Script
176
- - [setup env](setup-env.md) - ([credit Eric Shapiro] - additional info on contents of .env file
177
210
  - [this file](README.md)
178
211
  - [named colors](named-colors.md)
179
212
  - [sandbox](sandbox.md)
@@ -56,7 +56,7 @@ const tli = () => {
56
56
  deleteTempFile(id)
57
57
 
58
58
  }
59
- tli()
59
+ // tli()
60
60
 
61
61
  const tx2 = () => {
62
62
  let doc = DocumentApp.create("abc")
@@ -0,0 +1,7 @@
1
+ import '../../main.js';
2
+ console.log(process.env.LOG_DESTINATION)
3
+ Logger.__logDestination="BOTH"
4
+ console.log(Logger.__destination)
5
+ console.log (Logger.__cloudLogLink)
6
+
7
+
@@ -3,90 +3,94 @@ import * as cheerio from 'cheerio';
3
3
  import fs from 'fs/promises';
4
4
  import { URL } from 'url';
5
5
 
6
- const baseUrl = "https://developers.google.com/apps-script/reference/"
6
+ const baseUrl = "https://developers.google.com/apps-script/reference/script/"
7
7
  const outputFile = './gas-inventory.json';
8
8
 
9
9
  const visited = new Map();
10
10
  const queue = [baseUrl]
11
11
 
12
+ // the urls are kebab cased
12
13
  const kebabCamel = (s) => {
13
14
  return s.replace(/([-][a-z])/ig, ($1) => {
14
15
  return $1.toUpperCase()
15
16
  .replace('-', '')
16
17
  });
17
18
  };
18
- async function scrape() {
19
- const inventory = {};
20
19
 
21
- while (queue.length > 0) {
22
- const url = queue.shift();
23
- console.log(`Scraping ${url}`);
24
- const response = await got(url);
25
- const $ = cheerio.load(response.body);
26
- const serviceName = $('h1').text().split('\n').map(f=>f.replace(/^\s+/,'').trim()).filter (f=>f)[0];
27
- console.log ('...starting service name',serviceName)
28
- inventory[serviceName] = {
29
- name: serviceName,
30
- link: url,
31
- classes: {},
32
- };
20
+ function debugPageStructure($) {
21
+ console.log('=== PAGE STRUCTURE DEBUG ===');
33
22
 
34
- $('a').each((i, el) => {
35
- const href = $(el).attr('href');
36
- if (href && href.startsWith('/apps-script/reference')) {
37
- const absoluteUrl = new URL(href, url).href;
38
- const parentUrl = absoluteUrl.replace (/#.*/, '')
39
- const method = kebabCamel(absoluteUrl.match(/#(.*)/) || ['', ''][1])
40
- if (!visited.has(parentUrl)) {
41
- console.log ('...adding to queue',parentUrl)
42
- queue.push(parentUrl);
43
- visited.set(parentUrl, {
44
- url: parentUrl,
45
- methods: new Map ()
46
- });
47
- }
48
- if (method) {
49
- visited.get(parentUrl).methods.set(absoluteUrl, {
50
- url: absoluteUrl,
51
- method
52
- })
53
- }
54
- }
55
- });
23
+ // Check if main elements exist
24
+ console.log('#main-content exists:', $('#main-content').length > 0);
25
+ console.log('devsite-content exists:', $('devsite-content').length > 0);
26
+ console.log('article exists:', $('article').length > 0);
27
+ console.log('h1 elements:', $('h1').length);
56
28
 
57
- // Extract classes and methods
58
- $('h2').each((i, el) => {
59
- const className = $(el).text().trim();
60
- if (className) {
61
- console.log ('...found class',className,'for service',serviceName)
62
- inventory[serviceName].classes[className] = {
63
- methods: {},
64
- properties: {},
65
- };
66
-
67
- // Find the table of methods for the current class
68
- const methodsTable = $(el).nextAll('table').first();
69
- methodsTable.find('tbody tr').each((j, row) => {
70
- const methodName = $(row).find('td').eq(1).text().trim();
71
- const methodLink = $(row).find('td').eq(1).find('a').attr('href');
72
- const returnType = $(row).find('td').eq(0).text().trim();
29
+ // Show all h1 texts
30
+ $('h1').each((i, el) => {
31
+ console.log(`H1[${i}]:`, $(el).text().trim());
32
+ });
73
33
 
74
- if (methodName) {
75
- inventory[serviceName].classes[className].methods[methodName] = {
76
- link: methodLink ? new URL(methodLink, url).href : '',
77
- returns: {
78
- type: returnType,
79
- link: '' // to be extracted later
80
- }
81
- };
82
- }
83
- });
84
- }
34
+ // Show structure of main-content
35
+ const mainContent = $('#main-content');
36
+ if (mainContent.length > 0) {
37
+ console.log('Main-content children:', mainContent.children().length);
38
+ mainContent.children().each((i, el) => {
39
+ console.log(`Child ${i}:`, el.tagName, $(el).attr('class') || $(el).attr('id'));
85
40
  });
86
41
  }
42
+ }
87
43
 
88
- await fs.writeFile(outputFile, JSON.stringify(inventory, null, 2));
89
- console.log(`Inventory saved to ${outputFile}`);
44
+
45
+ const getNextCheer = async (u) => {
46
+ console.log(`Scraping ${u}`);
47
+ const response = await got(u);
48
+ return cheerio.load(response.body);
90
49
  }
91
50
 
92
- scrape().catch(console.error);
51
+ const getHrefs = ($) => {
52
+ return $('a').map((i, el) => {
53
+ const r = $(el).attr('href');
54
+ if (r) {
55
+ const absolute = new URL(r, baseUrl).href;
56
+ // skip references to elsewhere and blank anchors
57
+ if (absolute.startsWith(baseUrl) && !absolute.endsWith('#')) return absolute;
58
+ }
59
+ }).get();
60
+ }
61
+
62
+
63
+ const s2 = async () => {
64
+ const umap = new Map();
65
+ const uset = new Set()
66
+ while (queue.length) {
67
+
68
+ const url = queue.shift();
69
+ console.log('working on url', url)
70
+ const $ = await getNextCheer(url);
71
+ debugPageStructure($);
72
+
73
+ // get all the hrefs
74
+ getHrefs($).forEach(f => {
75
+ // so a url could have an anchor for a method so we need to strip that out
76
+ const u = new URL(f);
77
+ const method = u.hash.substring(1);
78
+ const classUrl = u.origin + '/' + u.pathname
79
+ const methodUrl = u.href
80
+ if (!umap.has(classUrl)) {
81
+ umap.set(classUrl, {
82
+ parents: new Map(),
83
+ classUrl,
84
+ methods: new Map()
85
+ })
86
+ console.log ('pushing', classUrl)
87
+ queue.push (classUrl)
88
+ }
89
+ const ob = umap.get(classUrl)
90
+ ob.methods.set(method, { method, methodUrl })
91
+ ob.parents.set(url, { url })
92
+ console.log('adding', f)
93
+ })
94
+ }
95
+ }
96
+ s2().catch(console.error)
package/package.json CHANGED
@@ -5,9 +5,9 @@
5
5
  "dependencies": {
6
6
  "@google-cloud/logging": "^11.2.1",
7
7
  "@mcpher/fake-gasenum": "^1.0.2",
8
- "@mcpher/unit": "^1.1.11",
9
8
  "@sindresorhus/is": "^7.0.1",
10
9
  "archiver": "^7.0.1",
10
+ "exceljs": "^4.4.0",
11
11
  "fast-json-stable-stringify": "^2.1.0",
12
12
  "get-stream": "^9.0.1",
13
13
  "googleapis": "^157.0.0",
@@ -17,56 +17,58 @@
17
17
  "keyv": "^5.5.0",
18
18
  "keyv-file": "^5.1.3",
19
19
  "mime": "^4.0.7",
20
+ "sinon": "^21.0.0",
20
21
  "sleep-synchronously": "^2.0.0",
21
22
  "subsume": "^4.0.0",
22
23
  "to-readable-stream": "^4.0.0",
23
- "unzipper": "^0.12.3"
24
+ "unzipper": "^0.12.3",
25
+ "yoctocolors": "^2.1.2"
24
26
  },
25
27
  "type": "module",
26
28
  "scripts": {
27
- "test": "cp mainlocal.js main.js && node --env-file=.env ./test/test.js",
28
- "testdrive": "cp mainlocal.js main.js && node --env-file=.env ./test/testdrive.js execute",
29
- "testsheetsdatavalidations": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsdatavalidations.js execute",
30
- "testsheetspermissions": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetspermissions.js execute",
31
- "testsheetsvalues": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsvalues.js execute",
32
- "testsheets": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheets.js execute",
33
- "testfetch": "cp mainlocal.js main.js && node --env-file=.env ./test/testfetch.js execute",
34
- "testsession": "cp mainlocal.js main.js && node --env-file=.env ./test/testsession.js execute",
35
- "testutilities": "cp mainlocal.js main.js && node --env-file=.env ./test/testutilities.js execute",
36
- "teststores": "cp mainlocal.js main.js && node --env-file=.env ./test/teststores.js execute",
37
- "testscriptapp": "cp mainlocal.js main.js && node --env-file=.env ./test/testscriptapp.js execute",
38
- "testfiddler": "cp mainlocal.js main.js && node --env-file=.env ./test/testfiddler.js execute",
39
- "testenums": "cp mainlocal.js main.js && node --env-file=.env ./test/testenums.js execute",
40
- "testsheetssets": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetssets.js execute",
41
- "testsheetsvui": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsvui.js execute",
42
- "testsheetsdeveloper": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsdeveloper.js execute",
43
- "testsheetsexotics": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsexotics.js execute",
44
- "testsheetsdata": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsdata.js execute",
45
- "testdocsadv": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsadv.js execute",
46
- "testslidesadv": "cp mainlocal.js main.js && node --env-file=.env ./test/testslidesadv.js execute",
47
- "testform": "cp mainlocal.js main.js && node --env-file=.env ./test/testform.js execute",
48
- "testformsadv": "cp mainlocal.js main.js && node --env-file=.env ./test/testformsadv.js execute",
49
- "testdocs": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocs.js execute",
50
- "testslides": "cp mainlocal.js main.js && node --env-file=.env ./test/testslides.js execute",
51
- "testdocsnext": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsnext.js execute",
52
- "testsheetstext": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetstext.js execute",
53
- "testsheetsrange": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsrange.js execute",
54
- "testdocslistitems": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocslistitems.js execute",
55
- "testdocsall": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsall.js",
56
- "testdocsheaders": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsheaders.js execute",
57
- "testdocsfooters": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsfooters.js execute",
58
- "testdocsfootnotes": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsfootnotes.js execute",
59
- "testdocsimages": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsimages.js execute",
60
- "testdocsstyles": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsstyles.js execute",
61
- "testsandbox": "cp mainlocal.js main.js && node --env-file=.env ./test/testsandbox.js execute",
62
- "testgmail": "cp mainlocal.js main.js && node --env-file=.env ./test/testgmail.js execute",
63
- "testlogger": "cp mainlocal.js main.js && node --env-file=.env ./test/testlogger.js execute",
64
- "pub": "cp mainlocal.js main.js && npm publish --access public"
29
+ "test": "cp mainlocal.js main.js && node ./test/test.js",
30
+ "testdrive": "cp mainlocal.js main.js && node ./test/testdrive.js execute",
31
+ "testsheetsdatavalidations": "cp mainlocal.js main.js && node ./test/testsheetsdatavalidations.js execute",
32
+ "testsheetspermissions": "cp mainlocal.js main.js && node ./test/testsheetspermissions.js execute",
33
+ "testsheetsvalues": "cp mainlocal.js main.js && node ./test/testsheetsvalues.js execute",
34
+ "testsheets": "cp mainlocal.js main.js && node ./test/testsheets.js execute",
35
+ "testfetch": "cp mainlocal.js main.js && node ./test/testfetch.js execute",
36
+ "testsession": "cp mainlocal.js main.js && node ./test/testsession.js execute",
37
+ "testutilities": "cp mainlocal.js main.js && node ./test/testutilities.js execute",
38
+ "teststores": "cp mainlocal.js main.js && node ./test/teststores.js execute",
39
+ "testscriptapp": "cp mainlocal.js main.js && node ./test/testscriptapp.js execute",
40
+ "testfiddler": "cp mainlocal.js main.js && node ./test/testfiddler.js execute",
41
+ "testenums": "cp mainlocal.js main.js && node ./test/testenums.js execute",
42
+ "testsheetssets": "cp mainlocal.js main.js && node ./test/testsheetssets.js execute",
43
+ "testsheetsvui": "cp mainlocal.js main.js && node ./test/testsheetsvui.js execute",
44
+ "testsheetsdeveloper": "cp mainlocal.js main.js && node ./test/testsheetsdeveloper.js execute",
45
+ "testsheetsexotics": "cp mainlocal.js main.js && node ./test/testsheetsexotics.js execute",
46
+ "testsheetsdata": "cp mainlocal.js main.js && node ./test/testsheetsdata.js execute",
47
+ "testdocsadv": "cp mainlocal.js main.js && node ./test/testdocsadv.js execute",
48
+ "testslidesadv": "cp mainlocal.js main.js && node ./test/testslidesadv.js execute",
49
+ "testform": "cp mainlocal.js main.js && node ./test/testform.js execute",
50
+ "testformsadv": "cp mainlocal.js main.js && node ./test/testformsadv.js execute",
51
+ "testdocs": "cp mainlocal.js main.js && node ./test/testdocs.js execute",
52
+ "testslides": "cp mainlocal.js main.js && node ./test/testslides.js execute",
53
+ "testdocsnext": "cp mainlocal.js main.js && node ./test/testdocsnext.js execute",
54
+ "testsheetstext": "cp mainlocal.js main.js && node ./test/testsheetstext.js execute",
55
+ "testsheetsrange": "cp mainlocal.js main.js && node ./test/testsheetsrange.js execute",
56
+ "testdocslistitems": "cp mainlocal.js main.js && node ./test/testdocslistitems.js execute",
57
+ "testdocsall": "cp mainlocal.js main.js && node ./test/testdocsall.js",
58
+ "testdocsheaders": "cp mainlocal.js main.js && node ./test/testdocsheaders.js execute",
59
+ "testdocsfooters": "cp mainlocal.js main.js && node ./test/testdocsfooters.js execute",
60
+ "testdocsfootnotes": "cp mainlocal.js main.js && node ./test/testdocsfootnotes.js execute",
61
+ "testdocsimages": "cp mainlocal.js main.js && node ./test/testdocsimages.js execute",
62
+ "testdocsstyles": "cp mainlocal.js main.js && node ./test/testdocsstyles.js execute",
63
+ "testsandbox": "cp mainlocal.js main.js && node ./test/testsandbox.js execute",
64
+ "testgmail": "cp mainlocal.js main.js && node ./test/testgmail.js execute",
65
+ "testlogger": "cp mainlocal.js main.js && node ./test/testlogger.js execute",
66
+ "pub": "cp mainlocal.js main.js && npm publish --access public"
65
67
  },
66
68
  "name": "@mcpher/gas-fakes",
67
- "version": "1.0.21",
69
+ "version": "1.0.23",
68
70
  "license": "MIT",
69
- "main": "main.js",
71
+ "main": "main.js && node ",
70
72
  "description": "A proof of concept implementation of Apps Script Environment on Node",
71
73
  "repository": "github:brucemcpherson/gas-fakes",
72
74
  "homepage": "https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/"
package/src/index.js CHANGED
@@ -1,4 +1,9 @@
1
+ import colors from 'yoctocolors'
1
2
  import './services/scriptapp/app.js'
3
+
4
+ import {default as pf } from '../package.json' with { type: 'json' };
5
+ console.log (`You are using ${colors.blueBright(pf.name)} package version ${colors.greenBright(pf.version)}`)
6
+ ScriptApp.__gasFakesVersion = pf.version
2
7
  import './services/driveapp/app.js'
3
8
  import './services/logger/app.js'
4
9
  import './services/urlfetchapp/app.js'
@@ -16,7 +21,3 @@ import './services/documentapp/app.js'
16
21
  import './services/advforms/app.js'
17
22
  import './services/formapp/app.js'
18
23
  import './services/slidesapp/app.js'
19
-
20
- // force initilialization of scriptApp - which is always needed
21
- // scriptapp will always be fake here
22
- ScriptApp.isFake
@@ -23,6 +23,11 @@ export class FakeLogger {
23
23
  * @type {string}
24
24
  */
25
25
  this.__destination = (process.env.LOG_DESTINATION || 'CONSOLE').toUpperCase();
26
+ this.__startedLoggingAt = new Date()
27
+ this.__cloudLogLink = 'No cloud logging enabled - use Logger.__logDestination = "CLOUD" or "BOTH" to enable'
28
+ if (['CLOUD','BOTH'].includes(this.__destination)) {
29
+ writeToCloudOrConsole("...Initializing cloud logging", this)
30
+ }
26
31
  }
27
32
 
28
33
  /**
@@ -58,6 +63,7 @@ export class FakeLogger {
58
63
  const message = format(formatOrData, ...values);
59
64
  this.__log.push(message);
60
65
 
66
+
61
67
  // Pass the destination and instance context to the writer function.
62
68
  writeToCloudOrConsole(message, this);
63
69
 
@@ -81,8 +87,16 @@ export class FakeLogger {
81
87
  if (['CONSOLE', 'CLOUD', 'BOTH', 'NONE'].includes(destination.toUpperCase())) {
82
88
  this.__destination = destination.toUpperCase();
83
89
  }
90
+ // we need to send a logger message to initialize and test
91
+ this.log (`...setting destination to ${this.__destination}`)
84
92
  return this;
85
93
  }
94
+
95
+ // to allow code compatibility without error on apps script, we should use the setter since it'll be undefined on Apps live Script
96
+ set __logDestination (destination) {
97
+ return this.__setLogDestination(destination);
98
+ }
99
+
86
100
  }
87
101
 
88
102
  /**
@@ -96,21 +110,23 @@ export const newFakeLogger = (...args) => Proxies.guard(new FakeLogger(...args))
96
110
  * @private
97
111
  * @param {FakeLogger} loggerInstance The logger instance, to access its destination.
98
112
  */
99
- const writeToCloudOrConsole = (message, loggerInstance) => {
113
+ const writeToCloudOrConsole = (message, loggerInstance) => {
100
114
  const logDestination = loggerInstance.__destination;
101
115
  const useConsoleLogging = logDestination === 'CONSOLE' || logDestination === 'BOTH';
102
116
  const useCloudLogging = logDestination === 'CLOUD' || logDestination === 'BOTH';
117
+ const logName = 'gas-fakes/console_logs';
118
+ const projectId = ScriptApp.__projectId;
119
+ const scriptId = ScriptApp.getScriptId();
120
+ const userId = ScriptApp.__userId;
103
121
 
104
122
  // Lazy-initialize the cloud logger if needed.
105
123
  const initializeCloudLogging = () => {
106
124
  if (cloudLog) return; // Already initialized.
107
125
  try {
108
- const projectId = ScriptApp.__projectId;
109
126
  if (!projectId) {
110
127
  throw new Error('Could not determine Google Cloud Project ID for logging.');
111
128
  }
112
129
  const logging = new Logging({ projectId });
113
- const logName = 'gas-fakes/console_logs';
114
130
  cloudLog = logging.log(logName);
115
131
  console.info(`gas-fakes: Cloud Logging is enabled, writing to log "${logName}".`);
116
132
  } catch (err) {
@@ -145,8 +161,8 @@ const writeToCloudOrConsole = (message, loggerInstance) => {
145
161
  // not running on a specific GCP compute service.
146
162
  resource: { type: 'global' },
147
163
  labels: {
148
- 'gas-fakes-scriptId': ScriptApp.getScriptId(),
149
- 'gas-fakes-userId': ScriptApp.__userId,
164
+ 'gas-fakes-scriptId': scriptId,
165
+ 'gas-fakes-userId': userId,
150
166
  },
151
167
  jsonPayload: { message: message },
152
168
  severity: 'INFO',
@@ -156,7 +172,36 @@ const writeToCloudOrConsole = (message, loggerInstance) => {
156
172
  // his allows us to catch errors, such as a disabled API or permission issues.
157
173
  // however we are firing and forgetting so an error might appear later on in execution
158
174
  if (cloudLog) {
159
- cloudLog.write(cloudLog.entry(metadata)).catch(err=> console.error('gas-fakes: Failed to write to Cloud Logging.', err.message))
175
+ // filter on user/script/priject and run times
176
+ // we'll extend the end date filter to a little more than now
177
+ const aLittleMore = 7 * 1000
178
+ const endDate = new Date(new Date().getTime() + aLittleMore)
179
+ const startDate = loggerInstance.__startedLoggingAt
180
+
181
+ const base = `https://console.cloud.google.com/logs/query;`
182
+
183
+ // B. Build the RAW LQL Filter String (NO "query=" prefix here)
184
+ const lqlParts = [
185
+ // LogName filter
186
+ `logName="projects/${projectId}/logs/${logName.replace('/', '%2F')}"`,
187
+ // Labels filter
188
+ ...Object.keys(metadata.labels).map(k => `jsonPayload.labels.${k}="${metadata.labels[k]}"`),
189
+ // Date filter (recommended for query stability)
190
+ `timestamp>="${startDate.toISOString()}"`,
191
+ `timestamp<="${endDate.toISOString()}"`
192
+ ];
193
+ const fullLQLQuery = lqlParts.join(' AND ');
194
+
195
+ // C. Encode ONLY the filter content, then prepend the literal 'query='
196
+ const encodedQueryParam = `query=${encodeURIComponent(fullLQLQuery)}`;
197
+
198
+ // D. Create the timeRange parameter (correct casing)
199
+ const timeRangeParam = `;timeRange=${startDate.getTime()}/${endDate.getTime()}`;
200
+
201
+ // E. Assemble the final URL
202
+ loggerInstance.__cloudLogLink = base + encodedQueryParam + `?project=${projectId}`;
203
+ // fire and forget
204
+ cloudLog.write(cloudLog.entry(metadata)).catch(err => console.error('gas-fakes: Failed to write to Cloud Logging.', err.message))
160
205
  }
161
206
  }
162
- };
207
+ };
@@ -0,0 +1,47 @@
1
+ import { Proxies } from "../../support/proxies.js";
2
+
3
+ /**
4
+ * create a new FakeOverGridImage instance
5
+ * @param {...any} args
6
+ * @returns {FakeOverGridImage}
7
+ */
8
+ export const newFakeOverGridImage = (...args) => {
9
+ return Proxies.guard(new FakeOverGridImage(...args));
10
+ };
11
+
12
+ /**
13
+ * basic fake FakeOverGridImage
14
+ * @class FakeOverGridImage
15
+ */
16
+ export class FakeOverGridImage {
17
+ /**
18
+ * @constructor
19
+ * @param {Sheet} sheet
20
+ * @param {Object} obj
21
+ * @returns {FakeOverGridImage}
22
+ */
23
+ constructor(sheet, obj) {
24
+ this.sheet = sheet;
25
+ this.object = obj;
26
+ }
27
+
28
+ getAnchorCell() {
29
+ return this.sheet.getRange(this.object.row + 1, this.object.col + 1);
30
+ }
31
+
32
+ getAnchorCellXOffset() {
33
+ return this.object.anchorCellXOffset;
34
+ }
35
+
36
+ getAnchorCellYOffset() {
37
+ return this.object.anchorCellYOffset;
38
+ }
39
+
40
+ getWidth() {
41
+ return this.object.width;
42
+ }
43
+
44
+ getHeight() {
45
+ return this.object.height;
46
+ }
47
+ }
@@ -13,6 +13,10 @@ import { FakeTextFinder, newFakeTextFinder } from "./faketextfinder.js";
13
13
 
14
14
  import { newFakeNamedRange } from "./fakenamedrange.js";
15
15
  import { newFakeProtection } from "./fakeprotection.js";
16
+ import { newFakeOverGridImage } from "./fakeovergridimage.js";
17
+
18
+ import { Syncit } from "../../support/syncit.js";
19
+ // import ExcelJS from "exceljs"; // When test_getImages() is used, use this.
16
20
 
17
21
  const { is, isEnum } = Utils;
18
22
 
@@ -35,7 +39,7 @@ export class FakeSheet {
35
39
  "insertChart",
36
40
  "removeChart",
37
41
  "updateChart",
38
- "getImages",
42
+ // "getImages",
39
43
  "insertImage",
40
44
  "removeImage",
41
45
  // "getNamedRanges",
@@ -1063,6 +1067,30 @@ export class FakeSheet {
1063
1067
  return [];
1064
1068
  }
1065
1069
 
1070
+ // This function is used for testing getImages without a worker.
1071
+ // async test_getImages() {
1072
+ // const url = `https://docs.google.com/spreadsheets/export?exportFormat=xlsx&id=${
1073
+ // this.__parent.__meta.spreadsheetId
1074
+ // }&access_token=${ScriptApp.getOAuthToken()}`;
1075
+ // const res = UrlFetchApp.fetch(url);
1076
+ // const buf = new Uint8Array(res.getBlob()._data).buffer;
1077
+ // const workbook = new ExcelJS.Workbook();
1078
+ // await workbook.xlsx.load(buf);
1079
+ // const worksheet = workbook.worksheets[this.getIndex() - 1];
1080
+ // const images = worksheet.getImages();
1081
+ // console.log(images);
1082
+ // }
1083
+
1084
+ getImages() {
1085
+ const url = `https://docs.google.com/spreadsheets/export?exportFormat=xlsx&id=${
1086
+ this.__parent.__meta.spreadsheetId
1087
+ }&access_token=${ScriptApp.getOAuthToken()}`;
1088
+ const idx = this.getIndex() - 1;
1089
+ const ar = Syncit.fxGetImagesFromXlsx({ url, idx });
1090
+ const sheet = this;
1091
+ return ar.map((e) => newFakeOverGridImage(sheet, e));
1092
+ }
1093
+
1066
1094
  __batchUpdate({ spreadsheetId, requests }) {
1067
1095
  return Sheets.Spreadsheets.batchUpdate({ requests }, spreadsheetId);
1068
1096
  }
@@ -0,0 +1,41 @@
1
+ import got from "got";
2
+ import ExcelJS from "exceljs";
3
+
4
+ async function __getWorkbook(url) {
5
+ const res = await got(url, { responseType: "buffer" });
6
+ let buf;
7
+ if (res.rawBody && Buffer.isBuffer(res.rawBody)) {
8
+ buf = res.rawBody;
9
+ } else if (res.body && Buffer.isBuffer(res.body)) {
10
+ buf = res.body;
11
+ } else {
12
+ throw new Error(res);
13
+ }
14
+ const workbook = new ExcelJS.Workbook();
15
+ await workbook.xlsx.load(buf);
16
+ return workbook;
17
+ }
18
+
19
+ export const sxGetImagesFromXlsx = async (_, { url, idx }) => {
20
+ const workbook = await __getWorkbook(url);
21
+ const worksheet = workbook.worksheets[idx];
22
+ const ar = worksheet.getImages().reduce((arr, image) => {
23
+ const imageId = image.imageId;
24
+ if (image.range.tl.nativeColOff != 0 && image.range.tl.nativeRowOff != 0) {
25
+ const col = image.range.tl.nativeCol;
26
+ const row = image.range.tl.nativeRow;
27
+ const { width, height } = image.range.ext;
28
+ arr.push({
29
+ imageId,
30
+ row,
31
+ col,
32
+ width,
33
+ height,
34
+ anchorCellXOffset: image.range.tl.nativeColOff,
35
+ anchorCellYOffset: image.range.tl.nativeRowOff,
36
+ });
37
+ }
38
+ return arr;
39
+ }, []);
40
+ return ar;
41
+ };