@mcpher/gas-fakes 2.3.10 → 2.3.13

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.
Files changed (50) hide show
  1. package/README.md +14 -14
  2. package/gas-fakes.js +1 -0
  3. package/gf_agent/scripts/builder.js +26 -1
  4. package/package.json +1 -1
  5. package/src/cli/lib-manager.js +14 -4
  6. package/src/cli/setup.js +17 -4
  7. package/src/services/chartsapp/fakechartsapp.js +6 -1
  8. package/src/services/documentapp/elementhelpers.js +27 -0
  9. package/src/services/documentapp/fakeelement.js +8 -0
  10. package/src/services/documentapp/fakeparagraph.js +14 -0
  11. package/src/services/documentapp/faketext.js +174 -4
  12. package/src/services/driveapp/driveiterators.js +53 -8
  13. package/src/services/driveapp/fakedriveapp.js +49 -15
  14. package/src/services/driveapp/fakedrivefile.js +105 -0
  15. package/src/services/driveapp/fakedrivefolder.js +8 -0
  16. package/src/services/driveapp/fakedrivemeta.js +68 -16
  17. package/src/services/driveapp/fakefolderapp.js +19 -6
  18. package/src/services/enums/chartsenums.js +53 -20
  19. package/src/services/enums/driveenums.js +1 -0
  20. package/src/services/slidesapp/fakeautofit.js +23 -15
  21. package/src/services/slidesapp/fakecolorscheme.js +160 -0
  22. package/src/services/slidesapp/fakelayout.js +11 -1
  23. package/src/services/slidesapp/fakemaster.js +10 -0
  24. package/src/services/slidesapp/fakepresentation.js +27 -0
  25. package/src/services/slidesapp/fakeslide.js +9 -0
  26. package/src/services/slidesapp/faketextrange.js +6 -11
  27. package/src/services/spreadsheetapp/chartenummapping.js +15 -0
  28. package/src/services/spreadsheetapp/fakebooleancondition.js +119 -0
  29. package/src/services/spreadsheetapp/fakecellimage.js +42 -0
  30. package/src/services/spreadsheetapp/fakecellimagebuilder.js +59 -0
  31. package/src/services/spreadsheetapp/fakeconditionalformatrule.js +55 -0
  32. package/src/services/spreadsheetapp/fakeconditionalformatrulebuilder.js +330 -0
  33. package/src/services/spreadsheetapp/fakedevelopermetadata.js +32 -4
  34. package/src/services/spreadsheetapp/fakedevelopermetadatalocation.js +27 -5
  35. package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +155 -21
  36. package/src/services/spreadsheetapp/fakegradientcondition.js +71 -0
  37. package/src/services/spreadsheetapp/fakesheet.js +63 -0
  38. package/src/services/spreadsheetapp/fakesheetrange.js +12 -1
  39. package/src/services/spreadsheetapp/fakespreadsheet.js +30 -11
  40. package/src/services/spreadsheetapp/fakespreadsheetapp.js +21 -3
  41. package/src/services/urlfetchapp/app.js +33 -1
  42. package/src/support/fileiterators.js +3 -1
  43. package/src/support/filesharers.js +7 -3
  44. package/src/support/peeker.js +8 -2
  45. package/src/support/sheetutils.js +1 -0
  46. package/src/support/sxdrive.js +26 -15
  47. package/src/support/syncit.js +4 -6
  48. package/src/support/workersync/synchronizer.js +24 -4
  49. package/src/support/workersync/worker.js +13 -2
  50. package/summarize_advanced.js +0 -69
@@ -0,0 +1,71 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { makeColorFromApi } from '../common/fakecolorbuilder.js';
3
+ import { InterpolationType } from '../enums/sheetsenums.js';
4
+
5
+ export const newFakeGradientCondition = (...args) => {
6
+ return Proxies.guard(new FakeGradientCondition(...args));
7
+ };
8
+
9
+ export class FakeGradientCondition {
10
+ constructor(apiGradient) {
11
+ this.__apiGradient = apiGradient || {};
12
+ }
13
+
14
+ __getColor(point) {
15
+ if (!point) return null;
16
+ if (point.colorStyle) return makeColorFromApi(point.colorStyle);
17
+ if (point.color) return makeColorFromApi({ rgbColor: point.color });
18
+ return null;
19
+ }
20
+
21
+ __getType(point) {
22
+ if (!point || !point.type) return null;
23
+ return InterpolationType[point.type] || null;
24
+ }
25
+
26
+ __getValue(point) {
27
+ if (!point) return "";
28
+ if (point.type === 'MIN' || point.type === 'MAX') return "";
29
+ return point.value !== undefined ? point.value : "";
30
+ }
31
+
32
+ getMaxColorObject() {
33
+ return this.__getColor(this.__apiGradient.maxpoint);
34
+ }
35
+
36
+ getMaxType() {
37
+ return this.__getType(this.__apiGradient.maxpoint);
38
+ }
39
+
40
+ getMaxValue() {
41
+ return this.__getValue(this.__apiGradient.maxpoint);
42
+ }
43
+
44
+ getMidColorObject() {
45
+ return this.__getColor(this.__apiGradient.midpoint);
46
+ }
47
+
48
+ getMidType() {
49
+ return this.__getType(this.__apiGradient.midpoint);
50
+ }
51
+
52
+ getMidValue() {
53
+ return this.__getValue(this.__apiGradient.midpoint);
54
+ }
55
+
56
+ getMinColorObject() {
57
+ return this.__getColor(this.__apiGradient.minpoint);
58
+ }
59
+
60
+ getMinType() {
61
+ return this.__getType(this.__apiGradient.minpoint);
62
+ }
63
+
64
+ getMinValue() {
65
+ return this.__getValue(this.__apiGradient.minpoint);
66
+ }
67
+
68
+ toString() {
69
+ return 'GradientCondition';
70
+ }
71
+ }
@@ -5,6 +5,7 @@ import { Utils } from "../../support/utils.js";
5
5
  import { SheetUtils } from "../../support/sheetutils.js";
6
6
  import { batchUpdate, makeSheetsGridRange } from "./sheetrangehelpers.js";
7
7
  import { newFakeFilter } from "./fakefilter.js";
8
+ import { newFakeConditionalFormatRule } from "./fakeconditionalformatrule.js";
8
9
  import { newFakePivotTable } from "./fakepivottable.js";
9
10
  import { newFakeBanding } from "./fakebanding.js";
10
11
  import { newFakeDeveloperMetadataFinder } from "./fakedevelopermetadatafinder.js";
@@ -378,6 +379,68 @@ export class FakeSheet {
378
379
  : null;
379
380
  }
380
381
 
382
+ /**
383
+ * clearConditionalFormatRules() https://developers.google.com/apps-script/reference/spreadsheet/sheet#clearconditionalformatrules
384
+ * Removes all conditional format rules from the sheet. Equivalent to calling setConditionalFormatRules(rules) with an empty array as input.
385
+ */
386
+ clearConditionalFormatRules() {
387
+ this.setConditionalFormatRules([]);
388
+ }
389
+
390
+ /**
391
+ * getConditionalFormatRules() https://developers.google.com/apps-script/reference/spreadsheet/sheet#getconditionalformatrules
392
+ * Get all conditional format rules in this sheet.
393
+ * @returns {FakeConditionalFormatRule[]} An array of all rules in the sheet.
394
+ */
395
+ getConditionalFormatRules() {
396
+ const meta = this.getParent().__getMetaProps(
397
+ `sheets(conditionalFormats,properties.sheetId)`
398
+ );
399
+ const sheetMeta = meta.sheets.find(
400
+ (s) => s.properties.sheetId === this.getSheetId()
401
+ );
402
+ const rules = sheetMeta?.conditionalFormats || [];
403
+ return rules.map(rule => newFakeConditionalFormatRule(rule, this));
404
+ }
405
+
406
+ /**
407
+ * setConditionalFormatRules(rules) https://developers.google.com/apps-script/reference/spreadsheet/sheet#setconditionalformatrulesrules
408
+ * Replaces all currently existing conditional format rules in the sheet with the input rules. Rules are evaluated in their input order.
409
+ * @param {FakeConditionalFormatRule[]} rules The array of conditional format rules to set.
410
+ */
411
+ setConditionalFormatRules(rules) {
412
+ const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.setConditionalFormatRules");
413
+ if (nargs !== 1 || !Array.isArray(rules)) matchThrow();
414
+
415
+ const requests = [];
416
+
417
+ // Delete existing rules
418
+ const existingRules = this.getConditionalFormatRules();
419
+ const existingCount = existingRules.length;
420
+ for (let i = existingCount - 1; i >= 0; i--) {
421
+ requests.push({
422
+ deleteConditionalFormatRule: {
423
+ sheetId: this.getSheetId(),
424
+ index: i
425
+ }
426
+ });
427
+ }
428
+
429
+ // Add new rules
430
+ rules.forEach((rule, index) => {
431
+ requests.push({
432
+ addConditionalFormatRule: {
433
+ rule: rule.__apiRule,
434
+ index: index
435
+ }
436
+ });
437
+ });
438
+
439
+ if (requests.length > 0) {
440
+ Sheets.Spreadsheets.batchUpdate({ requests: requests }, this.__parent.getId());
441
+ }
442
+ }
443
+
381
444
  getPivotTables() {
382
445
  const meta = this.getParent().__getMetaProps(
383
446
  `sheets(data(rowData(values(pivotTable))),properties.sheetId)`
@@ -2611,6 +2611,17 @@ skipFilteredRows Boolean Whether to avoid clearing filtered rows.
2611
2611
  // options = { valueInputOption: "RAW" },
2612
2612
  options = { valueInputOption: "USER_ENTERED" },
2613
2613
  }) {
2614
+ // Intercept FakeCellImage objects and convert to =IMAGE(url) formula
2615
+ const processedValues = values.map(row =>
2616
+ row.map(cell => {
2617
+ if (cell && typeof cell === 'object' && cell.toString() === 'CellImage') {
2618
+ const url = cell._properties?.sourceUrl || null;
2619
+ return url ? `=IMAGE("${url}")` : "";
2620
+ }
2621
+ return cell;
2622
+ })
2623
+ );
2624
+
2614
2625
  const range = single
2615
2626
  ? this.__getRangeWithSheet(this.__getTopLeft())
2616
2627
  : this.__getWithSheet();
@@ -2620,7 +2631,7 @@ skipFilteredRows Boolean Whether to avoid clearing filtered rows.
2620
2631
  {
2621
2632
  majorDimension: "ROWS",
2622
2633
  range,
2623
- values,
2634
+ values: processedValues,
2624
2635
  },
2625
2636
  ],
2626
2637
  };
@@ -152,17 +152,6 @@ export class FakeSpreadsheet {
152
152
  "deleteColumn",
153
153
  "getImages",
154
154
  "find",
155
- "sort",
156
- "addEditor",
157
- "addEditors",
158
- "addViewers",
159
- "addViewer",
160
- "removeViewer",
161
- "removeEditor",
162
-
163
- // these convert to a pdf so we'll need to figure out how to do that
164
- // probably using the Drive export
165
- "getAs",
166
155
  "getBlob",
167
156
  ];
168
157
 
@@ -181,6 +170,36 @@ export class FakeSpreadsheet {
181
170
  return this.__getFirstSheet();
182
171
  }
183
172
 
173
+ addViewer(emailAddress) {
174
+ this.__file.addViewer(emailAddress);
175
+ return this;
176
+ }
177
+
178
+ addEditor(emailAddress) {
179
+ this.__file.addEditor(emailAddress);
180
+ return this;
181
+ }
182
+
183
+ addViewers(emailAddresses) {
184
+ this.__file.addViewers(emailAddresses);
185
+ return this;
186
+ }
187
+
188
+ addEditors(emailAddresses) {
189
+ this.__file.addEditors(emailAddresses);
190
+ return this;
191
+ }
192
+
193
+ removeViewer(emailAddress) {
194
+ this.__file.removeViewer(emailAddress);
195
+ return this;
196
+ }
197
+
198
+ removeEditor(emailAddress) {
199
+ this.__file.removeEditor(emailAddress);
200
+ return this;
201
+ }
202
+
184
203
 
185
204
  addDeveloperMetadata(key, value, visibility) {
186
205
  const { nargs, matchThrow } = signatureArgs(
@@ -12,8 +12,10 @@ import { newFakeRichTextValueBuilder } from "../common/fakerichtextvalue.js";
12
12
  import { newFakeTextStyleBuilder } from "../common/faketextstylebuilder.js";
13
13
  import { newFakeFilterCriteriaBuilder } from "./fakefiltercriteriabuilder.js";
14
14
  import { newFakeDataValidationBuilder } from "./fakedatavalidationbuilder.js";
15
+ import { newFakeConditionalFormatRuleBuilder } from "./fakeconditionalformatrulebuilder.js";
15
16
  import { newFakeDataSourceSpecBuilder } from "./fakedatasourcespecbuilder.js";
16
- import { FakeTextFinder, newFakeTextFinder } from "./faketextfinder.js";
17
+ import { newFakeTextFinder } from "./faketextfinder.js";
18
+ import { newFakeCellImageBuilder } from "./fakecellimagebuilder.js";
17
19
 
18
20
  import * as Enums from "../enums/sheetsenums.js";
19
21
 
@@ -79,7 +81,6 @@ export class FakeSpreadsheetApp {
79
81
 
80
82
  const props = [
81
83
  "getActive",
82
- "newConditionalFormatRule",
83
84
  "getActiveSheet",
84
85
  "getCurrentCell",
85
86
  "getActiveRange",
@@ -89,7 +90,6 @@ export class FakeSpreadsheetApp {
89
90
  "setCurrentCell",
90
91
  "setActiveRange",
91
92
  "setActiveRangeList",
92
- "newCellImage",
93
93
  "getUi",
94
94
  "open",
95
95
 
@@ -228,6 +228,15 @@ export class FakeSpreadsheetApp {
228
228
  return this.openById(match[1]);
229
229
  }
230
230
 
231
+ /**
232
+ * newCellImage() https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#newcellimage
233
+ * Creates a builder for a CellImage.
234
+ * @returns {FakeCellImageBuilder}
235
+ */
236
+ newCellImage() {
237
+ return newFakeCellImageBuilder();
238
+ }
239
+
231
240
  /**
232
241
  * newColor() https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#newcolor
233
242
  * Creates a builder for a Color.
@@ -237,6 +246,15 @@ export class FakeSpreadsheetApp {
237
246
  return newFakeColorBuilder();
238
247
  }
239
248
 
249
+ /**
250
+ * newConditionalFormatRule() https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#newconditionalformatrule
251
+ * Creates a builder for a conditional formatting rule.
252
+ * @returns {FakeConditionalFormatRuleBuilder}
253
+ */
254
+ newConditionalFormatRule() {
255
+ return newFakeConditionalFormatRuleBuilder();
256
+ }
257
+
240
258
  /**
241
259
  * newTextStyle() https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#newTextStyle()
242
260
  * Creates a builder for a TextStyle.
@@ -116,6 +116,37 @@ const fetchAll = (requests) => {
116
116
  return responses.map(({ data }) => responsify(data))
117
117
  }
118
118
 
119
+ const getRequest = (url, options = {}) => {
120
+ const defaultMethod = 'get';
121
+ const request = {
122
+ url: url,
123
+ method: (options.method || defaultMethod).toLowerCase(),
124
+ headers: options.headers || {},
125
+ };
126
+
127
+ // Apps Script defaults
128
+ if (options.contentType) {
129
+ request.contentType = options.contentType;
130
+ }
131
+
132
+ if (options.payload) {
133
+ request.payload = options.payload;
134
+ }
135
+
136
+ if (options.useIntranet !== undefined) {
137
+ request.useIntranet = options.useIntranet;
138
+ }
139
+
140
+ // Add standard fetch behavior
141
+ if (request.method === 'post' || request.method === 'put' || request.method === 'patch') {
142
+ if (!request.contentType) {
143
+ request.contentType = 'application/x-www-form-urlencoded';
144
+ }
145
+ }
146
+
147
+ return request;
148
+ }
149
+
119
150
 
120
151
  // This will eventually hold a proxy for DriveApp
121
152
  let _app = null
@@ -131,7 +162,8 @@ if (typeof globalThis[name] === typeof undefined) {
131
162
  if (!_app) {
132
163
  _app = {
133
164
  fetch,
134
- fetchAll
165
+ fetchAll,
166
+ getRequest
135
167
  }
136
168
  }
137
169
  // this is the actual driveApp we'll return from the proxy
@@ -31,7 +31,9 @@ export const getPermissionIterator = ({
31
31
  pageToken = nextPageToken
32
32
 
33
33
  // format the results into the folder or file object
34
- tank = permissions
34
+ // we must copy the array because the cache returns a reference
35
+ // and splicing it would empty the cache for subsequent calls
36
+ tank = [...permissions]
35
37
  }
36
38
 
37
39
  // if we've got anything in the tank send back the oldest one
@@ -4,14 +4,18 @@ import { newFakeUser } from '../services/common/fakeuser.js';
4
4
 
5
5
  /**
6
6
  * get the file sharers
7
- * @returns {FakeUser} the file viewers
7
+ * @param {string} id the file id
8
+ * @param {string|string[]} roles the role or roles to filter by
9
+ * @returns {FakeUser[]} the file viewers
8
10
  */
9
- export const getSharers = (id, role) => {
11
+ export const getSharers = (id, roles) => {
12
+ const roleList = Array.isArray(roles) ? roles : [roles]
10
13
  const pit = getPermissionIterator({ id })
11
14
  const viewers = []
12
15
  while (pit.hasNext()) {
13
16
  const permission = pit.next()
14
- if (permission.role === role && permission.type === "user") viewers.push(makeUserFromPermission(permission))
17
+ // console.log(`...DEBUG: permission for ${id}:`, JSON.stringify(permission))
18
+ if (roleList.includes(permission.role) && permission.type === "user") viewers.push(makeUserFromPermission(permission))
15
19
  }
16
20
  return viewers
17
21
  }
@@ -8,13 +8,19 @@ class Peeker {
8
8
  /**
9
9
  * @constructor
10
10
  * @param {function} generator the generator function to add a hasNext() to
11
+ * @param {function} [continuationHandler] a function to return a continuation token
11
12
  * @returns {Peeker}
12
13
  */
13
- constructor(generator) {
14
+ constructor(generator, continuationHandler) {
14
15
  this.generator = generator
15
16
  // in order to be able to do a hasnext we have to actually get the value
16
17
  // this is the next value stored
17
18
  this.peeked = generator.next()
19
+ this.continuationHandler = continuationHandler
20
+ }
21
+
22
+ getContinuationToken() {
23
+ return this.continuationHandler ? this.continuationHandler(this.peeked) : null
18
24
  }
19
25
 
20
26
  /**
@@ -37,7 +43,7 @@ class Peeker {
37
43
  // instead of returning the next, we return the prepeeked next
38
44
  const value = this.peeked.value
39
45
  this.peeked = this.generator.next()
40
- return value
46
+ return value?.__fakeResolved ?? value
41
47
  }
42
48
  }
43
49
 
@@ -256,4 +256,5 @@ export const SheetUtils = {
256
256
  fromRange,
257
257
  a1ToR1C1,
258
258
  r1c1ToA1,
259
+ columnToLetter,
259
260
  }
@@ -550,25 +550,36 @@ const sxStreamer = async ({
550
550
  params,
551
551
  options = {},
552
552
  method = 'get' }) => {
553
- // this is the node drive service
554
- const drive = getDriveApiClient();
555
- const streamed = await drive.files[method](params, {
556
- responseType: 'stream',
557
- ...options
558
- })
559
- const response = responseSyncify(streamed)
560
- if (response.status === 200) {
561
- const buf = await getStreamAsBuffer(streamed.data)
562
- const data = Array.from(buf)
553
+ try {
554
+ // this is the node drive service
555
+ const drive = getDriveApiClient();
556
+ const streamed = await drive.files[method](params, {
557
+ responseType: 'stream',
558
+ ...options
559
+ })
560
+ const response = responseSyncify(streamed)
561
+ if (response.status === 200) {
562
+ const buf = await getStreamAsBuffer(streamed.data)
563
+ const data = Array.from(buf)
563
564
 
564
- return {
565
- data,
566
- response
565
+ return {
566
+ data,
567
+ response
568
+ }
569
+ } else {
570
+ return {
571
+ data: null,
572
+ response
573
+ }
567
574
  }
568
- } else {
575
+ } catch (err) {
576
+ // We don't want to crash the worker if the API call fails
577
+ // (e.g. exporting a non-exportable file)
578
+ const response = responseSyncify(err?.response || { status: err?.code || 500, statusText: err?.message })
569
579
  return {
570
580
  data: null,
571
- response
581
+ response,
582
+ error: err?.response?.data || err?.message || err
572
583
  }
573
584
  }
574
585
  }
@@ -70,10 +70,8 @@ const register = (id, cacher, result, allow404 = false, params) => {
70
70
  const { data, response } = result;
71
71
 
72
72
  if (checkResponseCacher(id, response, allow404, cacher)) {
73
- return {
74
- ...result,
75
- data: cacher.setEntry(id, params, data),
76
- };
73
+ cacher.setEntry(id, params, normalizeSerialization(data));
74
+ return result;
77
75
  } else {
78
76
  return result;
79
77
  }
@@ -147,7 +145,7 @@ const fxGeneric = ({
147
145
  const data = cacher.getEntry(resourceId, otherParams);
148
146
  if (data) {
149
147
  return {
150
- data,
148
+ data: normalizeSerialization(data),
151
149
  response: {
152
150
  status: 200,
153
151
  fromCache: true,
@@ -203,7 +201,7 @@ const fxDriveGet = ({
203
201
  const { cachedFile, good } = getFromFileCache(id, params.fields);
204
202
  if (good)
205
203
  return {
206
- data: cachedFile,
204
+ data: normalizeSerialization(cachedFile),
207
205
  // fake a good sxresponse
208
206
  response: {
209
207
  status: 200,
@@ -101,7 +101,12 @@ function cleanup() {
101
101
  }
102
102
 
103
103
  // The 'exit' event is for when the process is already shutting down normally.
104
- process.on('exit', cleanup);
104
+ process.on('exit', () => {
105
+ // Only terminate the worker if we aren't in the middle of an interactive CLI session
106
+ if (!process.env.GF_CLI_INTERACTIVE) {
107
+ cleanup();
108
+ }
109
+ });
105
110
  // By not listening for 'SIGINT', we allow Node.js to perform its default action,
106
111
  // which is to exit the process. The 'exit' event will then be fired to clean up the worker.
107
112
  process.on('SIGTERM', cleanup); // Catches `kill`
@@ -181,10 +186,25 @@ export function callSync(method, ...args) {
181
186
 
182
187
  if (hasError) {
183
188
  // Re-hydrate the error object on the main thread.
184
- const err = new Error(resultData.message);
189
+ // The error message from the worker might be a simple string or a JSON object from an API response
190
+ let message = resultData.message || resultString || "Unknown worker error";
191
+
192
+ // If it's a Drive API error object, the message is often nested
193
+ if (resultData.error?.message) {
194
+ message = resultData.error.message;
195
+ }
196
+
197
+ const err = new Error(message);
185
198
  err.stack = resultData.stack;
186
- // Copy other properties if they exist.
187
- Object.assign(err, resultData);
199
+
200
+ // Copy other important properties if they exist, except the ones we've handled
201
+ const skip = ['message', 'stack', 'name'];
202
+ Object.keys(resultData).forEach(key => {
203
+ if (!skip.includes(key)) {
204
+ err[key] = resultData[key];
205
+ }
206
+ });
207
+
188
208
  throw err;
189
209
  }
190
210
 
@@ -54,11 +54,23 @@ async function writeResult(result) {
54
54
  * @param {Error} error The error to write.
55
55
  */
56
56
  function writeError(error) {
57
- const errorObject = {
57
+ let errorObject = {
58
58
  message: error.message,
59
59
  stack: error.stack,
60
60
  name: error.name,
61
61
  };
62
+
63
+ // If the message is a JSON string (common for API errors that have been caught and re-thrown),
64
+ // try to parse it and merge the properties for better re-hydration on the main thread.
65
+ if (error.message && error.message.startsWith('{') && error.message.endsWith('}')) {
66
+ try {
67
+ const parsedMessage = JSON.parse(error.message);
68
+ errorObject = { ...errorObject, ...parsedMessage };
69
+ } catch (e) {
70
+ // Not valid JSON, keep as is
71
+ }
72
+ }
73
+
62
74
  const errorString = JSON.stringify(errorObject);
63
75
  const encodedError = textEncoder.encode(errorString);
64
76
 
@@ -142,7 +154,6 @@ parentPort.on('message', async (task) => {
142
154
  await writeResult(result);
143
155
 
144
156
  } catch (error) {
145
- syncError('An unhandled error occurred in the worker', error);
146
157
  writeError(error);
147
158
  } finally {
148
159
  // 3. Signal completion and wake up the main thread.
@@ -1,69 +0,0 @@
1
- import './main.js';
2
-
3
- try {
4
- console.log('Searching for emails from Martin...');
5
-
6
- // Search Gmail for messages from Martin (getting up to 10 recent threads)
7
- const threads = GmailApp.search('from:Martin', 0, 10);
8
-
9
- if (threads.length === 0) {
10
- console.log('No emails found from Martin.');
11
- process.exit(0);
12
- }
13
-
14
- console.log(`Found ${threads.length} threads. Creating summary doc...`);
15
-
16
- // Create the Document
17
- const doc = DocumentApp.create('Email Summary: Martin');
18
- const body = doc.getBody();
19
-
20
- // Add a title
21
- body.appendParagraph('Summary of Recent Emails from Martin')
22
- .setHeading(DocumentApp.ParagraphHeading.TITLE);
23
-
24
- body.appendParagraph(`Generated on: ${new Date().toLocaleString()}`);
25
- body.appendParagraph('');
26
-
27
- // We have to use the advanced Gmail service directly because gas-fakes'
28
- // FakeGmailMessage does not yet support getSubject(), getDate(), or getSnippet()
29
- for (const thread of threads) {
30
- // Get the raw thread resource from the Gmail API
31
- const threadResource = Gmail.Users.Threads.get('me', thread.getId());
32
- if (!threadResource.messages || threadResource.messages.length === 0) continue;
33
-
34
- const firstMsg = threadResource.messages[0];
35
-
36
- // Extract Subject from headers
37
- const headers = firstMsg.payload && firstMsg.payload.headers ? firstMsg.payload.headers : [];
38
- const subjectHeader = headers.find(h => h.name.toLowerCase() === 'subject');
39
- const dateHeader = headers.find(h => h.name.toLowerCase() === 'date');
40
-
41
- const subject = subjectHeader ? subjectHeader.value : 'No Subject';
42
- const date = dateHeader ? dateHeader.value : 'Unknown Date';
43
-
44
- body.appendParagraph(`Subject: ${subject}`)
45
- .setHeading(DocumentApp.ParagraphHeading.HEADING1);
46
-
47
- for (const msg of threadResource.messages) {
48
- const msgHeaders = msg.payload && msg.payload.headers ? msg.payload.headers : [];
49
- const msgDateHeader = msgHeaders.find(h => h.name.toLowerCase() === 'date');
50
- const msgDate = msgDateHeader ? msgDateHeader.value : 'Unknown Date';
51
-
52
- body.appendParagraph(`Date: ${msgDate}`)
53
- .setHeading(DocumentApp.ParagraphHeading.HEADING3);
54
-
55
- const snippet = msg.snippet || "No content.";
56
- body.appendParagraph(snippet);
57
- }
58
-
59
- body.appendParagraph(''); // Spacing
60
- }
61
-
62
- doc.saveAndClose();
63
-
64
- console.log(`Successfully created document "${doc.getName()}"`);
65
- console.log(`Document URL: ${doc.getUrl()}`);
66
-
67
- } catch (error) {
68
- console.error(`Error: ${error.message}`);
69
- }