@mcpher/gas-fakes 2.0.2 → 2.0.5

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
@@ -54,7 +54,20 @@ The optional `gasfakes.json` file holds various location and behavior parameters
54
54
  | properties | string | /tmp/gas-fakes/properties | gas-fakes uses a local file to emulate apps script's PropertiesService. This is where it should put the files. You may want to put it somewhere other than /tmp to avoid accidental deletion, but don't put it in a place that'll get commited to public git repo |
55
55
  | scriptId | string | from clasp, or some random value | If you have a clasp file, it'll pick up the scriptId from there. If not you can enter your scriptId manually, or just leave it to create a fake one. It's use for the moment is to return something useful from ScriptApp.getScriptId() and to partition the cache and properties stores |
56
56
 
57
+ ### Troubleshooting: Missing Environment Tags
57
58
 
59
+ If you see a warning or error like `Project '...' lacks an 'environment' tag`, it means your Google Cloud Organization has a policy requiring projects to be designated with an environment tag (e.g., `Development`, `Production`).
60
+
61
+ To resolve this, you need to bind an environment tag to your project. Replace `YOUR_ORG_ID` and `YOUR_PROJECT_ID` with your actual identifiers:
62
+
63
+ ```bash
64
+ # Bind the 'Development' environment tag to your project
65
+ gcloud resource-manager tags bindings create \
66
+ --tag-value=YOUR_ORG_ID/environment/Development \
67
+ --parent=//cloudresourcemanager.googleapis.com/projects/YOUR_PROJECT_ID
68
+ ```
69
+
70
+ *Note: The tag key `environment` and the value `Development` must already exist at the organization level. If they don't, you (or your admin) will need to create them first using `gcloud resource-manager tags keys create` and `gcloud resource-manager tags values create`.*
58
71
 
59
72
  ### Cloud Logging Integration
60
73
 
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "name": "@mcpher/gas-fakes",
35
35
  "author": "bruce mcpherson",
36
- "version": "2.0.2",
36
+ "version": "2.0.5",
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/setup.js CHANGED
@@ -153,7 +153,7 @@ export async function initializeConfiguration(options = {}) {
153
153
  name: "GOOGLE_CLOUD_PROJECT",
154
154
  message: "Enter your GCP Project ID",
155
155
  initial:
156
- existingConfig.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT,
156
+ existingConfig.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT || existingConfig.GCP_PROJECT_ID,
157
157
  },
158
158
  {
159
159
  type: "text",
@@ -483,7 +483,14 @@ export async function authenticateUser() {
483
483
  scopes += (extraScopes.startsWith(",") ? "" : ",") + extraScopes;
484
484
  }
485
485
  */
486
- const scopes = Array.from(new Set([DEFAULT_SCOPES.split(",").concat(EXTRA_SCOPES.split(","))].filter(d => d))).join(",")
486
+ const scopes = Array.from(
487
+ new Set([
488
+ ...(DEFAULT_SCOPES || "").split(","),
489
+ ...(EXTRA_SCOPES || "").split(","),
490
+ ])
491
+ )
492
+ .filter((s) => s)
493
+ .join(",");
487
494
  const driveAccessFlag = "--enable-gdrive-access";
488
495
 
489
496
  console.log(`...requesting scopes ${scopes}`);
@@ -630,7 +637,7 @@ export async function authenticateUser() {
630
637
  // Service Account Lifecycle
631
638
  let existing_sa = false
632
639
  try {
633
- execSync(`gcloud iam service-accounts describe "${sa_email}"`, { shell: true });
640
+ execSync(`gcloud iam service-accounts describe "${sa_email}"`, { stdio: "ignore", shell: true });
634
641
  existing_sa = true;
635
642
  } catch (error) {
636
643
  /* ignore */
@@ -4,6 +4,7 @@ import { registerFormItem } from './formitemregistry.js';
4
4
  import { signatureArgs } from '../../support/helpers.js';
5
5
  import { Utils } from '../../support/utils.js';
6
6
  import { ItemType } from '../enums/formsenums.js';
7
+ import { newFakeItemResponse } from './fakeitemresponse.js';
7
8
 
8
9
  export const newFakeCheckboxGridItem = (...args) => {
9
10
  return Proxies.guard(new FakeCheckboxGridItem(...args));
@@ -19,6 +20,35 @@ export class FakeCheckboxGridItem extends FakeFormItem {
19
20
  super(form, itemId);
20
21
  }
21
22
 
23
+ /**
24
+ * Creates a new ItemResponse for this checkbox grid item.
25
+ * @param {string[][]} responses a two-dimensional array of responses, where each inner array represents the selected columns for a row
26
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
27
+ */
28
+ createResponse(responses) {
29
+ const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxGridItem.createResponse');
30
+ if (nargs !== 1 || !Utils.is.array(responses) || !responses.every(Utils.is.array)) {
31
+ matchThrow('Invalid arguments: expected a two-dimensional string array.');
32
+ }
33
+
34
+ const questions = this.__resource.questionGroupItem?.questions || [];
35
+ if (responses.length !== questions.length) {
36
+ throw new Error(`The number of responses (${responses.length}) must match the number of rows (${questions.length}).`);
37
+ }
38
+
39
+ const answers = responses.map((rowResponses, index) => {
40
+ const questionId = questions[index].questionId;
41
+ return {
42
+ questionId,
43
+ textAnswers: {
44
+ answers: rowResponses.map(value => ({ value }))
45
+ }
46
+ };
47
+ });
48
+
49
+ return newFakeItemResponse(this, answers);
50
+ }
51
+
22
52
  /**
23
53
  * Gets the values for every column in the grid.
24
54
  * @returns {string[]} an array of column values
@@ -6,6 +6,7 @@ const { is } = Utils;
6
6
  import { registerFormItem } from './formitemregistry.js';
7
7
  import { newFakeChoice } from './fakechoice.js';
8
8
  import { ItemType } from '../enums/formsenums.js';
9
+ import { newFakeItemResponse } from './fakeitemresponse.js';
9
10
 
10
11
  export const newFakeCheckboxItem = (...args) => {
11
12
  return Proxies.guard(new FakeCheckboxItem(...args));
@@ -25,6 +26,28 @@ export class FakeCheckboxItem extends FakeChoiceItem {
25
26
  super(form, itemId);
26
27
  }
27
28
 
29
+ /**
30
+ * Creates a new ItemResponse for this checkbox item.
31
+ * @param {string[]} responses the selected choices
32
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
33
+ */
34
+ createResponse(responses) {
35
+ const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxItem.createResponse');
36
+ if (nargs !== 1 || !Utils.is.array(responses) || !responses.every(Utils.is.string)) {
37
+ matchThrow('Invalid arguments: expected a string array.');
38
+ }
39
+
40
+ const questionId = this.__resource.questionItem?.question?.questionId;
41
+ const answers = [{
42
+ questionId,
43
+ textAnswers: {
44
+ answers: responses.map(value => ({ value }))
45
+ }
46
+ }];
47
+
48
+ return newFakeItemResponse(this, answers);
49
+ }
50
+
28
51
  toString() {
29
52
  return 'CheckboxItem';
30
53
  }
@@ -0,0 +1,67 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { FakeFormItem } from './fakeformitem.js';
3
+ import { registerFormItem } from './formitemregistry.js';
4
+ import { ItemType } from '../enums/formsenums.js';
5
+ import { signatureArgs } from '../../support/helpers.js';
6
+ import { Utils } from '../../support/utils.js';
7
+ const { is } = Utils;
8
+ import { newFakeItemResponse } from './fakeitemresponse.js';
9
+
10
+ export const newFakeDateItem = (...args) => {
11
+ return Proxies.guard(new FakeDateItem(...args));
12
+ };
13
+
14
+ /**
15
+ * @class FakeDateItem
16
+ * @see https://developers.google.com/apps-script/reference/forms/date-item
17
+ */
18
+ export class FakeDateItem extends FakeFormItem {
19
+ constructor(...args) {
20
+ super(...args);
21
+ }
22
+
23
+ /**
24
+ * Creates a new ItemResponse for this date item.
25
+ * @param {Date} date the date
26
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
27
+ */
28
+ createResponse(date) {
29
+ const typeName = this.getType() === ItemType.DATETIME ? 'DateTimeItem' : 'DateItem';
30
+ const { nargs, matchThrow } = signatureArgs(arguments, `${typeName}.createResponse`);
31
+ if (nargs !== 1 || !is.date(date)) {
32
+ matchThrow('Invalid arguments: expected a Date object.');
33
+ }
34
+
35
+ const questionId = this.__resource.questionItem?.question?.questionId;
36
+
37
+ // Format date as YYYY-MM-DD or YYYY-MM-DDTHH:mm:ssZ for API?
38
+ // Actually, the Forms API expects text answers.
39
+ // For DATE, it's "YYYY-MM-DD". For DATETIME, it's "YYYY-MM-DD HH:mm".
40
+ const year = date.getFullYear();
41
+ const month = String(date.getMonth() + 1).padStart(2, '0');
42
+ const day = String(date.getDate()).padStart(2, '0');
43
+ let value = `${year}-${month}-${day}`;
44
+
45
+ if (this.getType() === ItemType.DATETIME) {
46
+ const hours = String(date.getHours()).padStart(2, '0');
47
+ const minutes = String(date.getMinutes()).padStart(2, '0');
48
+ value += ` ${hours}:${minutes}`;
49
+ }
50
+
51
+ const answers = [{
52
+ questionId,
53
+ textAnswers: {
54
+ answers: [{ value }]
55
+ }
56
+ }];
57
+
58
+ return newFakeItemResponse(this, answers);
59
+ }
60
+
61
+ toString() {
62
+ return this.getType() === ItemType.DATETIME ? 'DateTimeItem' : 'DateItem';
63
+ }
64
+ }
65
+
66
+ registerFormItem(ItemType.DATE, newFakeDateItem);
67
+ registerFormItem(ItemType.DATETIME, newFakeDateItem);
@@ -0,0 +1,54 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { FakeFormItem } from './fakeformitem.js';
3
+ import { registerFormItem } from './formitemregistry.js';
4
+ import { ItemType } from '../enums/formsenums.js';
5
+ import { signatureArgs } from '../../support/helpers.js';
6
+ import { Utils } from '../../support/utils.js';
7
+ const { is } = Utils;
8
+ import { newFakeItemResponse } from './fakeitemresponse.js';
9
+
10
+ export const newFakeDurationItem = (...args) => {
11
+ return Proxies.guard(new FakeDurationItem(...args));
12
+ };
13
+
14
+ /**
15
+ * @class FakeDurationItem
16
+ * @see https://developers.google.com/apps-script/reference/forms/duration-item
17
+ */
18
+ export class FakeDurationItem extends FakeFormItem {
19
+ constructor(...args) {
20
+ super(...args);
21
+ }
22
+
23
+ /**
24
+ * Creates a new ItemResponse for this duration item.
25
+ * @param {number} hours the hours
26
+ * @param {number} minutes the minutes
27
+ * @param {number} seconds the seconds
28
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
29
+ */
30
+ createResponse(hours, minutes, seconds) {
31
+ const { nargs, matchThrow } = signatureArgs(arguments, 'DurationItem.createResponse');
32
+ if (nargs !== 3 || !is.number(hours) || !is.number(minutes) || !is.number(seconds)) {
33
+ matchThrow('Invalid arguments: expected three numbers.');
34
+ }
35
+
36
+ const questionId = this.__resource.questionItem?.question?.questionId;
37
+ const value = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
38
+
39
+ const answers = [{
40
+ questionId,
41
+ textAnswers: {
42
+ answers: [{ value }]
43
+ }
44
+ }];
45
+
46
+ return newFakeItemResponse(this, answers);
47
+ }
48
+
49
+ toString() {
50
+ return 'DurationItem';
51
+ }
52
+ }
53
+
54
+ registerFormItem(ItemType.DURATION, newFakeDurationItem);
@@ -12,6 +12,9 @@ import { newFakeCheckboxItem } from './fakecheckboxitem.js';
12
12
  import { newFakeListItem } from './fakelistitem.js';
13
13
  import { newFakePageBreakItem } from './fakepagebreakitem.js';
14
14
  import { newFakeTextItem } from './faketextitem.js';
15
+ import { newFakeDateItem } from './fakedateitem.js';
16
+ import { newFakeTimeItem } from './faketimeitem.js';
17
+ import { newFakeDurationItem } from './fakedurationitem.js';
15
18
  import { signatureArgs } from '../../support/helpers.js';
16
19
  import { Utils } from '../../support/utils.js';
17
20
  const { is } = Utils
@@ -261,6 +264,93 @@ export class FakeForm {
261
264
  return this.__addItem(itemResource, newFakeTextItem);
262
265
  }
263
266
 
267
+ /**
268
+ * Appends a new question item that allows the respondent to enter a paragraph of text.
269
+ * @returns {import('./faketextitem.js').FakeTextItem} The new paragraph text item.
270
+ */
271
+ addParagraphTextItem() {
272
+ const itemResource = {
273
+ questionItem: {
274
+ question: {
275
+ textQuestion: {
276
+ paragraph: true,
277
+ },
278
+ },
279
+ },
280
+ };
281
+ return this.__addItem(itemResource, newFakeTextItem);
282
+ }
283
+
284
+ /**
285
+ * Appends a new question item that allows the respondent to choose a date.
286
+ * @returns {import('./fakedateitem.js').FakeDateItem} The new date item.
287
+ */
288
+ addDateItem() {
289
+ const itemResource = {
290
+ questionItem: {
291
+ question: {
292
+ dateQuestion: {
293
+ includeYear: true,
294
+ includeTime: false,
295
+ },
296
+ },
297
+ },
298
+ };
299
+ return this.__addItem(itemResource, newFakeDateItem);
300
+ }
301
+
302
+ /**
303
+ * Appends a new question item that allows the respondent to choose a date and time.
304
+ * @returns {import('./fakedateitem.js').FakeDateItem} The new date-time item.
305
+ */
306
+ addDateTimeItem() {
307
+ const itemResource = {
308
+ questionItem: {
309
+ question: {
310
+ dateQuestion: {
311
+ includeYear: true,
312
+ includeTime: true,
313
+ },
314
+ },
315
+ },
316
+ };
317
+ return this.__addItem(itemResource, newFakeDateItem);
318
+ }
319
+
320
+ /**
321
+ * Appends a new question item that allows the respondent to choose a time.
322
+ * @returns {import('./faketimeitem.js').FakeTimeItem} The new time item.
323
+ */
324
+ addTimeItem() {
325
+ const itemResource = {
326
+ questionItem: {
327
+ question: {
328
+ timeQuestion: {
329
+ duration: false,
330
+ },
331
+ },
332
+ },
333
+ };
334
+ return this.__addItem(itemResource, newFakeTimeItem);
335
+ }
336
+
337
+ /**
338
+ * Appends a new question item that allows the respondent to choose a duration.
339
+ * @returns {import('./fakedurationitem.js').FakeDurationItem} The new duration item.
340
+ */
341
+ addDurationItem() {
342
+ const itemResource = {
343
+ questionItem: {
344
+ question: {
345
+ timeQuestion: {
346
+ duration: true,
347
+ },
348
+ },
349
+ },
350
+ };
351
+ return this.__addItem(itemResource, newFakeDurationItem);
352
+ }
353
+
264
354
  /**
265
355
  * Gets the ID of the form's response destination.
266
356
  * @returns {string | null} The destination ID, or null if no destination is set.
@@ -4,6 +4,7 @@ import { registerFormItem } from './formitemregistry.js';
4
4
  import { signatureArgs } from '../../support/helpers.js';
5
5
  import { Utils } from '../../support/utils.js';
6
6
  import { ItemType } from '../enums/formsenums.js';
7
+ import { newFakeItemResponse } from './fakeitemresponse.js';
7
8
 
8
9
  export const newFakeGridItem = (...args) => {
9
10
  return Proxies.guard(new FakeGridItem(...args));
@@ -19,6 +20,35 @@ export class FakeGridItem extends FakeFormItem {
19
20
  super(form, itemId);
20
21
  }
21
22
 
23
+ /**
24
+ * Creates a new ItemResponse for this grid item.
25
+ * @param {string[]} responses an array of responses, where each element represents the selected column for a row
26
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
27
+ */
28
+ createResponse(responses) {
29
+ const { nargs, matchThrow } = signatureArgs(arguments, 'GridItem.createResponse');
30
+ if (nargs !== 1 || !Utils.is.array(responses) || !responses.every(Utils.is.string)) {
31
+ matchThrow('Invalid arguments: expected a string array.');
32
+ }
33
+
34
+ const questions = this.__resource.questionGroupItem?.questions || [];
35
+ if (responses.length !== questions.length) {
36
+ throw new Error(`The number of responses (${responses.length}) must match the number of rows (${questions.length}).`);
37
+ }
38
+
39
+ const answers = responses.map((rowResponse, index) => {
40
+ const questionId = questions[index].questionId;
41
+ return {
42
+ questionId,
43
+ textAnswers: {
44
+ answers: [{ value: rowResponse }]
45
+ }
46
+ };
47
+ });
48
+
49
+ return newFakeItemResponse(this, answers);
50
+ }
51
+
22
52
  /**
23
53
  * Gets the values for every column in the grid.
24
54
  * @returns {string[]} an array of column values
@@ -40,7 +70,7 @@ export class FakeGridItem extends FakeFormItem {
40
70
  if (!questions) {
41
71
  return [];
42
72
  }
43
- return questions.map(question => question.rowQuestion?.title || null).filter(f=>f);
73
+ return questions.map(question => question.rowQuestion?.title || null).filter(f => f);
44
74
  }
45
75
 
46
76
  /**
@@ -78,7 +78,11 @@ export class FakeItemResponse {
78
78
  );
79
79
 
80
80
  if (allTextAnswers.length === 0) {
81
- return '';
81
+ return itemType === 'CHECKBOX' ? [] : '';
82
+ }
83
+
84
+ if (itemType === 'CHECKBOX') {
85
+ return allTextAnswers.map(a => a.value);
82
86
  }
83
87
 
84
88
  // For items like grids, there can be multiple answer values. The live script joins them with a comma.
@@ -4,6 +4,9 @@ import { newFakeChoice } from './fakechoice.js';
4
4
  import { registerFormItem } from './formitemregistry.js';
5
5
  import { ItemType } from '../enums/formsenums.js';
6
6
  import { Utils } from '../../support/utils.js';
7
+ import { signatureArgs } from '../../support/helpers.js';
8
+ const { is } = Utils;
9
+ import { newFakeItemResponse } from './fakeitemresponse.js';
7
10
 
8
11
  export const newFakeListItem = (...args) => {
9
12
  return Proxies.guard(new FakeListItem(...args));
@@ -18,6 +21,28 @@ export class FakeListItem extends FakeFormItem {
18
21
  super(...args);
19
22
  }
20
23
 
24
+ /**
25
+ * Creates a new ItemResponse for this list item.
26
+ * @param {string} response the selected choice
27
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
28
+ */
29
+ createResponse(response) {
30
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.createResponse');
31
+ if (nargs !== 1 || !is.string(response)) {
32
+ matchThrow('Invalid arguments: expected a string.');
33
+ }
34
+
35
+ const questionId = this.__resource.questionItem?.question?.questionId;
36
+ const answers = [{
37
+ questionId,
38
+ textAnswers: {
39
+ answers: [{ value: response }]
40
+ }
41
+ }];
42
+
43
+ return newFakeItemResponse(this, answers);
44
+ }
45
+
21
46
  /**
22
47
  * Creates a new choice for this item.
23
48
  * @param {string} value The value for the new choice.
@@ -2,6 +2,10 @@ import { Proxies } from '../../support/proxies.js';
2
2
  import { FakeChoiceItem } from './fakechoiceitem.js';
3
3
  import { registerFormItem } from './formitemregistry.js';
4
4
  import { ItemType } from '../enums/formsenums.js';
5
+ import { signatureArgs } from '../../support/helpers.js';
6
+ import { Utils } from '../../support/utils.js';
7
+ const { is } = Utils;
8
+ import { newFakeItemResponse } from './fakeitemresponse.js';
5
9
 
6
10
  export const newFakeMultipleChoiceItem = (form, itemId) => {
7
11
  return Proxies.guard(new FakeMultipleChoiceItem(form, itemId));
@@ -16,6 +20,29 @@ export class FakeMultipleChoiceItem extends FakeChoiceItem {
16
20
  constructor(form, itemId) {
17
21
  super(form, itemId);
18
22
  }
23
+
24
+ /**
25
+ * Creates a new ItemResponse for this multiple choice item.
26
+ * @param {string} response the selected choice
27
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
28
+ */
29
+ createResponse(response) {
30
+ const { nargs, matchThrow } = signatureArgs(arguments, 'MultipleChoiceItem.createResponse');
31
+ if (nargs !== 1 || !is.string(response)) {
32
+ matchThrow('Invalid arguments: expected a string.');
33
+ }
34
+
35
+ const questionId = this.__resource.questionItem?.question?.questionId;
36
+ const answers = [{
37
+ questionId,
38
+ textAnswers: {
39
+ answers: [{ value: response }]
40
+ }
41
+ }];
42
+
43
+ return newFakeItemResponse(this, answers);
44
+ }
45
+
19
46
  toString() {
20
47
  return 'MultipleChoiceItem';
21
48
  }
@@ -5,6 +5,7 @@ import { ItemType } from '../enums/formsenums.js';
5
5
  import { signatureArgs } from '../../support/helpers.js';
6
6
  import { Utils } from '../../support/utils.js';
7
7
  const { is } = Utils;
8
+ import { newFakeItemResponse } from './fakeitemresponse.js';
8
9
 
9
10
  export const newFakeScaleItem = (...args) => {
10
11
  return Proxies.guard(new FakeScaleItem(...args));
@@ -20,6 +21,28 @@ export class FakeScaleItem extends FakeFormItem {
20
21
  super(form, itemId);
21
22
  }
22
23
 
24
+ /**
25
+ * Creates a new ItemResponse for this scale item.
26
+ * @param {number} response the selected value
27
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
28
+ */
29
+ createResponse(response) {
30
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ScaleItem.createResponse');
31
+ if (nargs !== 1 || !is.number(response)) {
32
+ matchThrow('Invalid arguments: expected a number.');
33
+ }
34
+
35
+ const questionId = this.__resource.questionItem?.question?.questionId;
36
+ const answers = [{
37
+ questionId,
38
+ textAnswers: {
39
+ answers: [{ value: response.toString() }]
40
+ }
41
+ }];
42
+
43
+ return newFakeItemResponse(this, answers);
44
+ }
45
+
23
46
  /**
24
47
  * Gets the lower bound of the scale.
25
48
  * @returns {Integer} the lower bound
@@ -1,5 +1,11 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
2
  import { FakeFormItem } from './fakeformitem.js';
3
+ import { registerFormItem } from './formitemregistry.js';
4
+ import { ItemType } from '../enums/formsenums.js';
5
+ import { signatureArgs } from '../../support/helpers.js';
6
+ import { Utils } from '../../support/utils.js';
7
+ const { is } = Utils;
8
+ import { newFakeItemResponse } from './fakeitemresponse.js';
3
9
 
4
10
  export const newFakeTextItem = (...args) => {
5
11
  return Proxies.guard(new FakeTextItem(...args));
@@ -14,7 +20,34 @@ export class FakeTextItem extends FakeFormItem {
14
20
  super(...args);
15
21
  }
16
22
 
23
+ /**
24
+ * Creates a new ItemResponse for this text item.
25
+ * @param {string} response the response text
26
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
27
+ */
28
+ createResponse(response) {
29
+ // Both TextItem and ParagraphTextItem have the same createResponse signature.
30
+ const typeName = this.getType() === ItemType.PARAGRAPH_TEXT ? 'ParagraphTextItem' : 'TextItem';
31
+ const { nargs, matchThrow } = signatureArgs(arguments, `${typeName}.createResponse`);
32
+ if (nargs !== 1 || !is.string(response)) {
33
+ matchThrow('Invalid arguments: expected a string.');
34
+ }
35
+
36
+ const questionId = this.__resource.questionItem?.question?.questionId;
37
+ const answers = [{
38
+ questionId,
39
+ textAnswers: {
40
+ answers: [{ value: response }]
41
+ }
42
+ }];
43
+
44
+ return newFakeItemResponse(this, answers);
45
+ }
46
+
17
47
  toString() {
18
- return 'TextItem';
48
+ return this.getType() === ItemType.PARAGRAPH_TEXT ? 'ParagraphTextItem' : 'TextItem';
19
49
  }
20
- }
50
+ }
51
+
52
+ registerFormItem(ItemType.TEXT, newFakeTextItem);
53
+ registerFormItem(ItemType.PARAGRAPH_TEXT, newFakeTextItem);
@@ -0,0 +1,53 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { FakeFormItem } from './fakeformitem.js';
3
+ import { registerFormItem } from './formitemregistry.js';
4
+ import { ItemType } from '../enums/formsenums.js';
5
+ import { signatureArgs } from '../../support/helpers.js';
6
+ import { Utils } from '../../support/utils.js';
7
+ const { is } = Utils;
8
+ import { newFakeItemResponse } from './fakeitemresponse.js';
9
+
10
+ export const newFakeTimeItem = (...args) => {
11
+ return Proxies.guard(new FakeTimeItem(...args));
12
+ };
13
+
14
+ /**
15
+ * @class FakeTimeItem
16
+ * @see https://developers.google.com/apps-script/reference/forms/time-item
17
+ */
18
+ export class FakeTimeItem extends FakeFormItem {
19
+ constructor(...args) {
20
+ super(...args);
21
+ }
22
+
23
+ /**
24
+ * Creates a new ItemResponse for this time item.
25
+ * @param {number} hour the hour
26
+ * @param {number} minute the minute
27
+ * @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
28
+ */
29
+ createResponse(hour, minute) {
30
+ const { nargs, matchThrow } = signatureArgs(arguments, 'TimeItem.createResponse');
31
+ if (nargs !== 2 || !is.number(hour) || !is.number(minute)) {
32
+ matchThrow('Invalid arguments: expected two numbers.');
33
+ }
34
+
35
+ const questionId = this.__resource.questionItem?.question?.questionId;
36
+ const value = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
37
+
38
+ const answers = [{
39
+ questionId,
40
+ textAnswers: {
41
+ answers: [{ value }]
42
+ }
43
+ }];
44
+
45
+ return newFakeItemResponse(this, answers);
46
+ }
47
+
48
+ toString() {
49
+ return 'TimeItem';
50
+ }
51
+ }
52
+
53
+ registerFormItem(ItemType.TIME, newFakeTimeItem);
@@ -8,4 +8,8 @@ import './fakecheckboxgriditem.js';
8
8
  import './fakescaleitem.js';
9
9
  import './fakepagebreakitem.js';
10
10
  import './fakelistitem.js';
11
+ import './faketextitem.js';
12
+ import './fakedateitem.js';
13
+ import './faketimeitem.js';
14
+ import './fakedurationitem.js';
11
15
  // Add other item types here as they are implemented (e.g., './faketextitem.js')
@@ -117,7 +117,7 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
117
117
  // 2. if AUTH_TYPE is ADC, use ADC
118
118
  // 3. if AUTH_TYPE is not set, use DWD if saName is present, else ADC
119
119
  const saName = process.env.GOOGLE_SERVICE_ACCOUNT_NAME
120
- const authType = process.env.AUTH_TYPE?.toLowerCase() || 'dwd'
120
+ const authType = process.env.AUTH_TYPE?.toLowerCase()
121
121
  const useDwd = authType === 'dwd' || (authType !== 'adc' && saName)
122
122
 
123
123
  if (!useDwd) {
@@ -127,6 +127,9 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
127
127
  })
128
128
  _sourceClient = _authClient
129
129
  } else {
130
+ if (!saName) {
131
+ throw new Error("Domain-Wide Delegation (DWD) requested or inferred, but GOOGLE_SERVICE_ACCOUNT_NAME is not set in environment.");
132
+ }
130
133
  mayLog(`...using service account: ${saName}`)
131
134
  const targetPrincipal = `${saName}@${_projectId}.iam.gserviceaccount.com`
132
135
  mayLog(`...attempting to use service account: ${targetPrincipal}`)
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import got from 'got';
9
9
  import { Auth } from './auth.js';
10
- import { syncError, syncLog } from './workersync/synclogger.js';
10
+ import { syncError, syncLog, syncWarn } from './workersync/synclogger.js';
11
11
  import { readFile, writeFile, mkdir } from 'fs/promises';
12
12
  import path from 'path'
13
13
 
@@ -85,8 +85,22 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
85
85
  // get the required scopes and set them
86
86
  const scopes = manifest.oauthScopes || []
87
87
 
88
+ // Force mandatory scopes for DWD if not already present
89
+ // These are required to get user identity information when using Domain-Wide Delegation
90
+ const mandatoryScopes = [
91
+ "openid",
92
+ "https://www.googleapis.com/auth/userinfo.email",
93
+ "https://www.googleapis.com/auth/cloud-platform"
94
+ ]
95
+
96
+ const scopeSet = new Set(scopes)
97
+ mandatoryScopes.forEach(scope => scopeSet.add(scope))
98
+ const finalScopes = Array.from(scopeSet)
99
+
100
+ syncLog(`...using scopes: ${finalScopes.join(', ')}`)
101
+
88
102
  // Initialize auth.
89
- const auth = await Auth.setAuth(scopes);
103
+ const auth = await Auth.setAuth(finalScopes);
90
104
 
91
105
  // static things we need to get into the main thread we can do now
92
106
  const projectId = Auth.getProjectId();
@@ -126,7 +140,7 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
126
140
  const allowedScopes = new Set(effectiveScopes)
127
141
  const missingScopes = scopes.filter(scope => !allowedScopes.has(scope))
128
142
  if (missingScopes.length > 0) {
129
- syncError(`...these scopes were asked for but not granted: ${missingScopes.join(', ')}`)
143
+ syncWarn(`...these scopes were asked for but not granted: ${missingScopes.join(', ')}. Note: Operations may still succeed if the 'https://www.googleapis.com/auth/cloud-platform' scope was granted.`)
130
144
  }
131
145
  return {
132
146
  // these will be the scopes we're allowed to get