runline 0.12.0 → 0.13.0

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.
@@ -22,14 +22,57 @@
22
22
  * sheet.deleteDimension — drop N rows or columns starting at an index
23
23
  * sheet.batchUpdate — raw passthrough to spreadsheets:batchUpdate
24
24
  *
25
+ * chart.add / chart.update / chart.delete
26
+ * namedRange.add / namedRange.delete
27
+ * protectedRange.add / protectedRange.delete
28
+ * conditionalFormat.add / conditionalFormat.delete
29
+ * dataValidation.set
30
+ *
25
31
  * Features: auto-column-add on update/upsert, multi-row
26
32
  * batchUpdate, RAW vs USER_ENTERED value input, appending via
27
33
  * values:append or PUT at lastRow+1, keepFirstRow on clear.
28
34
  * Filtering is intentionally the caller's job — `read` returns
29
35
  * raw rows / objects and callers post-filter.
30
36
  */
37
+ import * as t from "typebox";
31
38
  import { googleAccessToken } from "../../_shared/googleAuth.js";
39
+ import { Id, JsonValue, NonEmptyString, NonNegativeInteger, PositiveInteger, RawGoogleObject, StringArray, stringEnum, } from "../../_shared/googleSchemas.js";
32
40
  const ROW_NUMBER = "row_number";
41
+ const sheetReferenceSchema = t.Union([NonEmptyString, NonNegativeInteger]);
42
+ const cellValueSchema = t.Union([
43
+ t.String(),
44
+ t.Number(),
45
+ t.Boolean(),
46
+ t.Null(),
47
+ ]);
48
+ const objectRowSchema = t.Record(t.String(), JsonValue);
49
+ const rowsSchema = t.Union([
50
+ t.Array(t.Array(cellValueSchema)),
51
+ t.Array(objectRowSchema),
52
+ ]);
53
+ const valueInputOptionSchema = stringEnum(["RAW", "USER_ENTERED"]);
54
+ const handlingExtraDataSchema = stringEnum([
55
+ "ignore",
56
+ "error",
57
+ "insertInNewColumn",
58
+ ]);
59
+ const initialSheetSchema = t.Object({
60
+ title: NonEmptyString,
61
+ sheetId: t.Optional(NonNegativeInteger),
62
+ index: t.Optional(NonNegativeInteger),
63
+ sheetType: t.Optional(stringEnum(["GRID", "OBJECT", "DATA_SOURCE"])),
64
+ hidden: t.Optional(t.Boolean()),
65
+ rightToLeft: t.Optional(t.Boolean()),
66
+ gridProperties: t.Optional(t.Object({
67
+ rowCount: t.Optional(PositiveInteger),
68
+ columnCount: t.Optional(PositiveInteger),
69
+ frozenRowCount: t.Optional(NonNegativeInteger),
70
+ frozenColumnCount: t.Optional(NonNegativeInteger),
71
+ hideGridlines: t.Optional(t.Boolean()),
72
+ rowGroupControlAfter: t.Optional(t.Boolean()),
73
+ columnGroupControlAfter: t.Optional(t.Boolean()),
74
+ }, { additionalProperties: false })),
75
+ }, { additionalProperties: false });
33
76
  // ─── Auth ────────────────────────────────────────────────────────
34
77
  async function accessToken(ctx) {
35
78
  return googleAccessToken(ctx, "googleSheets", SCOPES);
@@ -60,7 +103,8 @@ async function sheetsRequest(ctx, method, path, body, qs, baseOverride) {
60
103
  },
61
104
  };
62
105
  if (body && Object.keys(body).length > 0) {
63
- init.headers["Content-Type"] = "application/json";
106
+ init.headers["Content-Type"] =
107
+ "application/json";
64
108
  init.body = JSON.stringify(body);
65
109
  }
66
110
  const res = await fetch(url.toString(), init);
@@ -322,7 +366,13 @@ function a1RangeToGridRange(a1, sheetId) {
322
366
  const startRow = parseInt(m[2], 10) - 1;
323
367
  const endCol = columnLetterToNumber(m[3]);
324
368
  const endRow = parseInt(m[4], 10);
325
- return { sheetId, startRowIndex: startRow, endRowIndex: endRow, startColumnIndex: startCol, endColumnIndex: endCol };
369
+ return {
370
+ sheetId,
371
+ startRowIndex: startRow,
372
+ endRowIndex: endRow,
373
+ startColumnIndex: startCol,
374
+ endColumnIndex: endCol,
375
+ };
326
376
  }
327
377
  export default function googleSheets(rl) {
328
378
  rl.setName("googleSheets");
@@ -410,20 +460,12 @@ export default function googleSheets(rl) {
410
460
  rl.registerAction("spreadsheet.create", {
411
461
  access: "write",
412
462
  description: "Create a new spreadsheet",
413
- inputSchema: {
414
- title: { type: "string", required: true },
415
- sheets: {
416
- type: "array",
417
- required: false,
418
- description: "[{title, hidden?}] — initial sheet tabs",
419
- },
420
- locale: { type: "string", required: false, description: "e.g. en_US" },
421
- autoRecalc: {
422
- type: "string",
423
- required: false,
424
- description: "ON_CHANGE | MINUTE | HOUR",
425
- },
426
- },
463
+ inputSchema: t.Object({
464
+ title: NonEmptyString,
465
+ sheets: t.Optional(t.Array(initialSheetSchema)),
466
+ locale: t.Optional(NonEmptyString),
467
+ autoRecalc: t.Optional(stringEnum(["ON_CHANGE", "MINUTE", "HOUR"])),
468
+ }, { additionalProperties: false }),
427
469
  async execute(input, ctx) {
428
470
  const p = (input ?? {});
429
471
  const body = {
@@ -444,16 +486,12 @@ export default function googleSheets(rl) {
444
486
  rl.registerAction("spreadsheet.get", {
445
487
  access: "read",
446
488
  description: "Get spreadsheet metadata (sheets, properties, optional grid data)",
447
- inputSchema: {
448
- spreadsheetId: { type: "string", required: true },
449
- includeGridData: { type: "boolean", required: false },
450
- ranges: {
451
- type: "array",
452
- required: false,
453
- description: "Limit the response to specific A1 ranges",
454
- },
455
- fields: { type: "string", required: false },
456
- },
489
+ inputSchema: t.Object({
490
+ spreadsheetId: Id,
491
+ includeGridData: t.Optional(t.Boolean()),
492
+ ranges: t.Optional(StringArray),
493
+ fields: t.Optional(NonEmptyString),
494
+ }, { additionalProperties: false }),
457
495
  async execute(input, ctx) {
458
496
  const p = (input ?? {});
459
497
  const qs = {};
@@ -469,7 +507,7 @@ export default function googleSheets(rl) {
469
507
  rl.registerAction("spreadsheet.delete", {
470
508
  access: "write",
471
509
  description: "Delete a spreadsheet (via Drive API — requires drive.file scope)",
472
- inputSchema: { spreadsheetId: { type: "string", required: true } },
510
+ inputSchema: t.Object({ spreadsheetId: Id }, { additionalProperties: false }),
473
511
  async execute(input, ctx) {
474
512
  const p = (input ?? {});
475
513
  await sheetsRequest(ctx, "DELETE", `/drive/v3/files/${p.spreadsheetId}`, undefined, undefined, "https://www.googleapis.com");
@@ -480,7 +518,7 @@ export default function googleSheets(rl) {
480
518
  rl.registerAction("sheet.listTabs", {
481
519
  access: "read",
482
520
  description: "List sheet tabs in a spreadsheet",
483
- inputSchema: { spreadsheetId: { type: "string", required: true } },
521
+ inputSchema: t.Object({ spreadsheetId: Id }, { additionalProperties: false }),
484
522
  async execute(input, ctx) {
485
523
  const p = (input ?? {});
486
524
  return getSheetProperties(ctx, p.spreadsheetId);
@@ -489,15 +527,15 @@ export default function googleSheets(rl) {
489
527
  rl.registerAction("sheet.addTab", {
490
528
  access: "write",
491
529
  description: "Add a new sheet tab",
492
- inputSchema: {
493
- spreadsheetId: { type: "string", required: true },
494
- title: { type: "string", required: true },
495
- index: { type: "number", required: false, description: "Position within the spreadsheet" },
496
- sheetId: { type: "number", required: false, description: "Custom tab ID (non-negative)" },
497
- hidden: { type: "boolean", required: false },
498
- rightToLeft: { type: "boolean", required: false },
499
- tabColor: { type: "string", required: false, description: "Hex RGB (e.g. #0aa55c)" },
500
- },
530
+ inputSchema: t.Object({
531
+ spreadsheetId: Id,
532
+ title: NonEmptyString,
533
+ index: t.Optional(NonNegativeInteger),
534
+ sheetId: t.Optional(NonNegativeInteger),
535
+ hidden: t.Optional(t.Boolean()),
536
+ rightToLeft: t.Optional(t.Boolean()),
537
+ tabColor: t.Optional(t.String({ pattern: "^#?[0-9a-fA-F]{6}$" })),
538
+ }, { additionalProperties: false }),
501
539
  async execute(input, ctx) {
502
540
  const p = (input ?? {});
503
541
  const properties = { title: p.title };
@@ -521,14 +559,10 @@ export default function googleSheets(rl) {
521
559
  rl.registerAction("sheet.deleteTab", {
522
560
  access: "write",
523
561
  description: "Delete a sheet tab",
524
- inputSchema: {
525
- spreadsheetId: { type: "string", required: true },
526
- sheet: {
527
- type: "string",
528
- required: true,
529
- description: "Tab title or numeric sheetId",
530
- },
531
- },
562
+ inputSchema: t.Object({
563
+ spreadsheetId: Id,
564
+ sheet: sheetReferenceSchema,
565
+ }, { additionalProperties: false }),
532
566
  async execute(input, ctx) {
533
567
  const p = (input ?? {});
534
568
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
@@ -542,40 +576,20 @@ export default function googleSheets(rl) {
542
576
  rl.registerAction("sheet.read", {
543
577
  access: "read",
544
578
  description: "Read values from a range. Returns a 2D array by default; set `asObjects=true` to pair rows with a header row and return objects.",
545
- inputSchema: {
546
- spreadsheetId: { type: "string", required: true },
547
- range: {
548
- type: "string",
549
- required: true,
550
- description: "Sheet tab name, or full A1 range (e.g. 'Sheet1!A1:C10')",
551
- },
552
- asObjects: { type: "boolean", required: false },
553
- headerRow: {
554
- type: "number",
555
- required: false,
556
- description: "1-indexed header row (default: 1, used with asObjects)",
557
- },
558
- dataStartRow: {
559
- type: "number",
560
- required: false,
561
- description: "1-indexed first data row (default: headerRow + 1)",
562
- },
563
- valueRenderOption: {
564
- type: "string",
565
- required: false,
566
- description: "FORMATTED_VALUE (default) | UNFORMATTED_VALUE | FORMULA",
567
- },
568
- dateTimeRenderOption: {
569
- type: "string",
570
- required: false,
571
- description: "FORMATTED_STRING (default) | SERIAL_NUMBER",
572
- },
573
- includeRowNumber: {
574
- type: "boolean",
575
- required: false,
576
- description: "Attach `row_number` to each object (asObjects only)",
577
- },
578
- },
579
+ inputSchema: t.Object({
580
+ spreadsheetId: Id,
581
+ range: NonEmptyString,
582
+ asObjects: t.Optional(t.Boolean()),
583
+ headerRow: t.Optional(PositiveInteger),
584
+ dataStartRow: t.Optional(PositiveInteger),
585
+ valueRenderOption: t.Optional(stringEnum([
586
+ "FORMATTED_VALUE",
587
+ "UNFORMATTED_VALUE",
588
+ "FORMULA",
589
+ ])),
590
+ dateTimeRenderOption: t.Optional(stringEnum(["FORMATTED_STRING", "SERIAL_NUMBER"])),
591
+ includeRowNumber: t.Optional(t.Boolean()),
592
+ }, { additionalProperties: false }),
579
593
  async execute(input, ctx) {
580
594
  const p = (input ?? {});
581
595
  const rows = await getValues(ctx, p.spreadsheetId, p.range, p.valueRenderOption ?? "FORMATTED_VALUE", p.dateTimeRenderOption ?? "FORMATTED_STRING");
@@ -599,39 +613,15 @@ export default function googleSheets(rl) {
599
613
  rl.registerAction("sheet.append", {
600
614
  access: "write",
601
615
  description: "Append rows to the bottom. Pass `rows` as arrays (matching columns) or objects (keyed by header names; extra keys become new columns by default).",
602
- inputSchema: {
603
- spreadsheetId: { type: "string", required: true },
604
- sheet: {
605
- type: "string",
606
- required: true,
607
- description: "Tab name or numeric sheetId",
608
- },
609
- rows: {
610
- type: "array",
611
- required: true,
612
- description: "Array of arrays, or array of objects",
613
- },
614
- headerRow: {
615
- type: "number",
616
- required: false,
617
- description: "1-indexed header row (default: 1, used when rows are objects)",
618
- },
619
- valueInputOption: {
620
- type: "string",
621
- required: false,
622
- description: "USER_ENTERED (default) | RAW",
623
- },
624
- handlingExtraData: {
625
- type: "string",
626
- required: false,
627
- description: "insertInNewColumn (default) | ignore | error",
628
- },
629
- useAppend: {
630
- type: "boolean",
631
- required: false,
632
- description: "Use values:append (safer w/ formulas but rewrites filters) instead of PUT at lastRow+1",
633
- },
634
- },
616
+ inputSchema: t.Object({
617
+ spreadsheetId: Id,
618
+ sheet: sheetReferenceSchema,
619
+ rows: rowsSchema,
620
+ headerRow: t.Optional(PositiveInteger),
621
+ valueInputOption: t.Optional(valueInputOptionSchema),
622
+ handlingExtraData: t.Optional(handlingExtraDataSchema),
623
+ useAppend: t.Optional(t.Boolean()),
624
+ }, { additionalProperties: false }),
635
625
  async execute(input, ctx) {
636
626
  const p = (input ?? {});
637
627
  const spreadsheetId = p.spreadsheetId;
@@ -694,20 +684,16 @@ export default function googleSheets(rl) {
694
684
  rl.registerAction("sheet.update", {
695
685
  access: "write",
696
686
  description: "Update rows matched by a key column (or by the synthetic 'row_number' field). Rows are objects; undefined/null values are skipped.",
697
- inputSchema: {
698
- spreadsheetId: { type: "string", required: true },
699
- sheet: { type: "string", required: true },
700
- rows: { type: "array", required: true, description: "Array of objects" },
701
- matchKey: {
702
- type: "string",
703
- required: true,
704
- description: "Column name to match on, or 'row_number'",
705
- },
706
- headerRow: { type: "number", required: false, description: "1-indexed (default: 1)" },
707
- dataStartRow: { type: "number", required: false },
708
- valueInputOption: { type: "string", required: false },
709
- handlingExtraData: { type: "string", required: false },
710
- },
687
+ inputSchema: t.Object({
688
+ spreadsheetId: Id,
689
+ sheet: sheetReferenceSchema,
690
+ rows: t.Array(objectRowSchema),
691
+ matchKey: NonEmptyString,
692
+ headerRow: t.Optional(PositiveInteger),
693
+ dataStartRow: t.Optional(PositiveInteger),
694
+ valueInputOption: t.Optional(valueInputOptionSchema),
695
+ handlingExtraData: t.Optional(handlingExtraDataSchema),
696
+ }, { additionalProperties: false }),
711
697
  async execute(input, ctx) {
712
698
  const p = (input ?? {});
713
699
  return runUpdateOrUpsert(ctx, p, false);
@@ -716,17 +702,17 @@ export default function googleSheets(rl) {
716
702
  rl.registerAction("sheet.appendOrUpdate", {
717
703
  access: "write",
718
704
  description: "Upsert rows. Rows whose match value is found are updated in place; rows with a missing or unknown match value are appended.",
719
- inputSchema: {
720
- spreadsheetId: { type: "string", required: true },
721
- sheet: { type: "string", required: true },
722
- rows: { type: "array", required: true, description: "Array of objects" },
723
- matchKey: { type: "string", required: true },
724
- headerRow: { type: "number", required: false },
725
- dataStartRow: { type: "number", required: false },
726
- valueInputOption: { type: "string", required: false },
727
- handlingExtraData: { type: "string", required: false },
728
- useAppend: { type: "boolean", required: false },
729
- },
705
+ inputSchema: t.Object({
706
+ spreadsheetId: Id,
707
+ sheet: sheetReferenceSchema,
708
+ rows: t.Array(objectRowSchema),
709
+ matchKey: NonEmptyString,
710
+ headerRow: t.Optional(PositiveInteger),
711
+ dataStartRow: t.Optional(PositiveInteger),
712
+ valueInputOption: t.Optional(valueInputOptionSchema),
713
+ handlingExtraData: t.Optional(handlingExtraDataSchema),
714
+ useAppend: t.Optional(t.Boolean()),
715
+ }, { additionalProperties: false }),
730
716
  async execute(input, ctx) {
731
717
  const p = (input ?? {});
732
718
  return runUpdateOrUpsert(ctx, p, true);
@@ -736,25 +722,70 @@ export default function googleSheets(rl) {
736
722
  rl.registerAction("sheet.clear", {
737
723
  access: "write",
738
724
  description: "Clear values from a sheet. Modes: wholeSheet (optional keepFirstRow) | rows | columns | range.",
739
- inputSchema: {
740
- spreadsheetId: { type: "string", required: true },
741
- sheet: { type: "string", required: true },
742
- mode: {
743
- type: "string",
744
- required: false,
745
- description: "wholeSheet (default) | rows | columns | range",
746
- },
747
- startRow: { type: "number", required: false, description: "1-indexed, for mode=rows" },
748
- rowCount: { type: "number", required: false, description: "for mode=rows" },
749
- startColumn: { type: "string", required: false, description: "A1 letter, for mode=columns" },
750
- columnCount: { type: "number", required: false, description: "for mode=columns" },
751
- range: { type: "string", required: false, description: "A1 region, for mode=range" },
752
- keepFirstRow: {
753
- type: "boolean",
754
- required: false,
755
- description: "mode=wholeSheet only — preserve row 1",
756
- },
757
- },
725
+ inputSchema: t.Object({
726
+ spreadsheetId: Id,
727
+ sheet: sheetReferenceSchema,
728
+ mode: t.Optional(stringEnum(["wholeSheet", "rows", "columns", "range"])),
729
+ startRow: t.Optional(PositiveInteger),
730
+ rowCount: t.Optional(PositiveInteger),
731
+ startColumn: t.Optional(t.String({ pattern: "^[A-Za-z]+$" })),
732
+ columnCount: t.Optional(PositiveInteger),
733
+ range: t.Optional(NonEmptyString),
734
+ keepFirstRow: t.Optional(t.Boolean()),
735
+ }, {
736
+ additionalProperties: false,
737
+ oneOf: [
738
+ {
739
+ properties: { mode: { const: "wholeSheet" } },
740
+ not: {
741
+ anyOf: [
742
+ { required: ["startRow"] },
743
+ { required: ["rowCount"] },
744
+ { required: ["startColumn"] },
745
+ { required: ["columnCount"] },
746
+ { required: ["range"] },
747
+ ],
748
+ },
749
+ },
750
+ {
751
+ properties: { mode: { const: "rows" } },
752
+ required: ["mode"],
753
+ not: {
754
+ anyOf: [
755
+ { required: ["startColumn"] },
756
+ { required: ["columnCount"] },
757
+ { required: ["range"] },
758
+ { required: ["keepFirstRow"] },
759
+ ],
760
+ },
761
+ },
762
+ {
763
+ properties: { mode: { const: "columns" } },
764
+ required: ["mode"],
765
+ not: {
766
+ anyOf: [
767
+ { required: ["startRow"] },
768
+ { required: ["rowCount"] },
769
+ { required: ["range"] },
770
+ { required: ["keepFirstRow"] },
771
+ ],
772
+ },
773
+ },
774
+ {
775
+ properties: { mode: { const: "range" } },
776
+ required: ["mode", "range"],
777
+ not: {
778
+ anyOf: [
779
+ { required: ["startRow"] },
780
+ { required: ["rowCount"] },
781
+ { required: ["startColumn"] },
782
+ { required: ["columnCount"] },
783
+ { required: ["keepFirstRow"] },
784
+ ],
785
+ },
786
+ },
787
+ ],
788
+ }),
758
789
  async execute(input, ctx) {
759
790
  const p = (input ?? {});
760
791
  const sheetName = typeof p.sheet === "number"
@@ -779,7 +810,9 @@ export default function googleSheets(rl) {
779
810
  }
780
811
  else if (mode === "range") {
781
812
  const region = String(p.range ?? "");
782
- range = region.includes("!") ? `${sheetName}!${region.split("!")[1]}` : `${sheetName}!${region}`;
813
+ range = region.includes("!")
814
+ ? `${sheetName}!${region.split("!")[1]}`
815
+ : `${sheetName}!${region}`;
783
816
  }
784
817
  else {
785
818
  range = sheetName;
@@ -799,17 +832,13 @@ export default function googleSheets(rl) {
799
832
  rl.registerAction("sheet.deleteDimension", {
800
833
  access: "write",
801
834
  description: "Delete a range of rows or columns",
802
- inputSchema: {
803
- spreadsheetId: { type: "string", required: true },
804
- sheet: { type: "string", required: true },
805
- dimension: { type: "string", required: true, description: "ROWS | COLUMNS" },
806
- startIndex: {
807
- type: "number",
808
- required: true,
809
- description: "1-indexed (row number) or column letter position",
810
- },
811
- count: { type: "number", required: false, description: "How many to delete (default: 1)" },
812
- },
835
+ inputSchema: t.Object({
836
+ spreadsheetId: Id,
837
+ sheet: sheetReferenceSchema,
838
+ dimension: stringEnum(["ROWS", "COLUMNS"]),
839
+ startIndex: PositiveInteger,
840
+ count: t.Optional(PositiveInteger),
841
+ }, { additionalProperties: false }),
813
842
  async execute(input, ctx) {
814
843
  const p = (input ?? {});
815
844
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
@@ -834,114 +863,153 @@ export default function googleSheets(rl) {
834
863
  rl.registerAction("sheet.batchUpdate", {
835
864
  access: "write",
836
865
  description: "Raw passthrough to spreadsheets:batchUpdate for anything this plugin doesn't expose directly (formatting, merges, conditional rules, …).",
837
- inputSchema: {
838
- spreadsheetId: { type: "string", required: true },
839
- requests: { type: "array", required: true },
840
- },
866
+ inputSchema: t.Object({
867
+ spreadsheetId: Id,
868
+ requests: t.Array(RawGoogleObject, { minItems: 1 }),
869
+ }, { additionalProperties: false }),
841
870
  async execute(input, ctx) {
842
871
  const p = (input ?? {});
843
872
  return spreadsheetBatchUpdate(ctx, p.spreadsheetId, p.requests);
844
873
  },
845
874
  });
846
- }
847
- /**
848
- * Shared implementation for `sheet.update` and `sheet.appendOrUpdate`.
849
- * Reads current headers + the key column, decides per row whether
850
- * it's an update or an append, issues one `values:batchUpdate` for
851
- * the updates, and (in upsert mode) falls through to the append
852
- * path for the rest.
853
- */
854
- async function runUpdateOrUpsert(ctx, p, upsert) {
855
- const spreadsheetId = p.spreadsheetId;
856
- const sheetIdOrName = p.sheet;
857
- const rows = p.rows;
858
- if (!Array.isArray(rows) || rows.length === 0) {
859
- return { updated: 0, appended: 0 };
860
- }
861
- const matchKey = p.matchKey;
862
- if (!matchKey)
863
- throw new Error("googleSheets: matchKey is required");
864
- const valueInputOption = p.valueInputOption ?? "USER_ENTERED";
865
- const allProps = await getSheetProperties(ctx, spreadsheetId);
866
- const prop = allProps.find((s) => s.title === sheetIdOrName || String(s.sheetId) === String(sheetIdOrName));
867
- if (!prop)
868
- throw new Error(`googleSheets: sheet "${sheetIdOrName}" not found`);
869
- const sheetName = prop.title;
870
- const sheetId = prop.sheetId;
871
- const headerRowIdx = Math.max(1, p.headerRow ?? 1) - 1;
872
- const dataStartRowIdx = Math.max(headerRowIdx + 1, (p.dataStartRow ?? headerRowIdx + 2) - 1);
873
- const handlingExtraData = p.handlingExtraData ?? "insertInNewColumn";
874
- // Headers come from FORMATTED_VALUE (cosmetic strings). The match
875
- // column needs UNFORMATTED_VALUE so "1,234" in input matches 1234
876
- // on the sheet (issue a second fetch with UNFORMATTED_VALUE).
877
- const sheetData = await getValues(ctx, spreadsheetId, sheetName, "FORMATTED_VALUE");
878
- let headers = sheetData[headerRowIdx] ?? [];
879
- let keyColumnValues = [];
880
- if (matchKey !== ROW_NUMBER) {
881
- const idx = headers.indexOf(matchKey);
882
- if (idx === -1) {
883
- if (!upsert)
884
- throw new Error(`googleSheets: match column "${matchKey}" not found`);
875
+ /**
876
+ * Shared implementation for `sheet.update` and `sheet.appendOrUpdate`.
877
+ * Reads current headers + the key column, decides per row whether
878
+ * it's an update or an append, issues one `values:batchUpdate` for
879
+ * the updates, and (in upsert mode) falls through to the append
880
+ * path for the rest.
881
+ */
882
+ async function runUpdateOrUpsert(ctx, p, upsert) {
883
+ const spreadsheetId = p.spreadsheetId;
884
+ const sheetIdOrName = p.sheet;
885
+ const rows = p.rows;
886
+ if (!Array.isArray(rows) || rows.length === 0) {
887
+ return { updated: 0, appended: 0 };
885
888
  }
886
- else {
887
- const unformatted = await getValues(ctx, spreadsheetId, sheetName, "UNFORMATTED_VALUE");
888
- keyColumnValues = unformatted.slice(dataStartRowIdx).map((r) => String(r[idx] ?? ""));
889
+ const matchKey = p.matchKey;
890
+ if (!matchKey)
891
+ throw new Error("googleSheets: matchKey is required");
892
+ const valueInputOption = p.valueInputOption ?? "USER_ENTERED";
893
+ const allProps = await getSheetProperties(ctx, spreadsheetId);
894
+ const prop = allProps.find((s) => s.title === sheetIdOrName ||
895
+ String(s.sheetId) === String(sheetIdOrName));
896
+ if (!prop)
897
+ throw new Error(`googleSheets: sheet "${sheetIdOrName}" not found`);
898
+ const sheetName = prop.title;
899
+ const sheetId = prop.sheetId;
900
+ const headerRowIdx = Math.max(1, p.headerRow ?? 1) - 1;
901
+ const dataStartRowIdx = Math.max(headerRowIdx + 1, (p.dataStartRow ?? headerRowIdx + 2) - 1);
902
+ const handlingExtraData = p.handlingExtraData ??
903
+ "insertInNewColumn";
904
+ // Headers come from FORMATTED_VALUE (cosmetic strings). The match
905
+ // column needs UNFORMATTED_VALUE so "1,234" in input matches 1234
906
+ // on the sheet (issue a second fetch with UNFORMATTED_VALUE).
907
+ const sheetData = await getValues(ctx, spreadsheetId, sheetName, "FORMATTED_VALUE");
908
+ let headers = sheetData[headerRowIdx] ?? [];
909
+ let keyColumnValues = [];
910
+ if (matchKey !== ROW_NUMBER) {
911
+ const idx = headers.indexOf(matchKey);
912
+ if (idx === -1) {
913
+ if (!upsert)
914
+ throw new Error(`googleSheets: match column "${matchKey}" not found`);
915
+ }
916
+ else {
917
+ const unformatted = await getValues(ctx, spreadsheetId, sheetName, "UNFORMATTED_VALUE");
918
+ keyColumnValues = unformatted
919
+ .slice(dataStartRowIdx)
920
+ .map((r) => String(r[idx] ?? ""));
921
+ }
889
922
  }
890
- }
891
- const prepared = prepareUpdateOrUpsert(rows, headers, keyColumnValues, dataStartRowIdx, matchKey, { upsert, handlingExtraData });
892
- // If we added new columns, extend the header row on the sheet first.
893
- if (prepared.newColumns.length > 0) {
894
- headers = [...headers, ...prepared.newColumns];
895
- await sheetsRequest(ctx, "PUT", `/v4/spreadsheets/${spreadsheetId}/values/${encodeA1(`${sheetName}!${headerRowIdx + 1}:${headerRowIdx + 1}`)}`, {
896
- range: `${sheetName}!${headerRowIdx + 1}:${headerRowIdx + 1}`,
897
- values: [headers],
898
- }, { valueInputOption });
899
- }
900
- // Qualify update ranges with the sheet name (prepareUpdateOrUpsert
901
- // returned bare A1 cells like "C5").
902
- const updateData = prepared.updateData.map((d) => ({
903
- range: `${sheetName}!${d.range}`,
904
- values: d.values,
905
- }));
906
- if (updateData.length > 0) {
907
- await batchValuesUpdate(ctx, spreadsheetId, updateData, valueInputOption);
908
- }
909
- if (upsert && prepared.appendData.length > 0) {
910
- const values = objectsToRows(prepared.appendData, headers);
911
- if (p.useAppend) {
912
- await sheetsRequest(ctx, "POST", `/v4/spreadsheets/${spreadsheetId}/values/${encodeA1(sheetName)}:append`, { range: sheetName, values }, { valueInputOption, insertDataOption: "INSERT_ROWS" });
923
+ const prepared = prepareUpdateOrUpsert(rows, headers, keyColumnValues, dataStartRowIdx, matchKey, { upsert, handlingExtraData });
924
+ // If we added new columns, extend the header row on the sheet first.
925
+ if (prepared.newColumns.length > 0) {
926
+ headers = [...headers, ...prepared.newColumns];
927
+ await sheetsRequest(ctx, "PUT", `/v4/spreadsheets/${spreadsheetId}/values/${encodeA1(`${sheetName}!${headerRowIdx + 1}:${headerRowIdx + 1}`)}`, {
928
+ range: `${sheetName}!${headerRowIdx + 1}:${headerRowIdx + 1}`,
929
+ values: [headers],
930
+ }, { valueInputOption });
913
931
  }
914
- else {
915
- const lastRow = sheetData.length;
916
- await appendEmptyRow(ctx, spreadsheetId, sheetId);
917
- const targetRange = `${sheetName}!${lastRow + 1}:${lastRow + values.length}`;
918
- await sheetsRequest(ctx, "PUT", `/v4/spreadsheets/${spreadsheetId}/values/${encodeA1(targetRange)}`, { range: targetRange, values }, { valueInputOption });
932
+ // Qualify update ranges with the sheet name (prepareUpdateOrUpsert
933
+ // returned bare A1 cells like "C5").
934
+ const updateData = prepared.updateData.map((d) => ({
935
+ range: `${sheetName}!${d.range}`,
936
+ values: d.values,
937
+ }));
938
+ if (updateData.length > 0) {
939
+ await batchValuesUpdate(ctx, spreadsheetId, updateData, valueInputOption);
919
940
  }
941
+ if (upsert && prepared.appendData.length > 0) {
942
+ const values = objectsToRows(prepared.appendData, headers);
943
+ if (p.useAppend) {
944
+ await sheetsRequest(ctx, "POST", `/v4/spreadsheets/${spreadsheetId}/values/${encodeA1(sheetName)}:append`, { range: sheetName, values }, { valueInputOption, insertDataOption: "INSERT_ROWS" });
945
+ }
946
+ else {
947
+ const lastRow = sheetData.length;
948
+ await appendEmptyRow(ctx, spreadsheetId, sheetId);
949
+ const targetRange = `${sheetName}!${lastRow + 1}:${lastRow + values.length}`;
950
+ await sheetsRequest(ctx, "PUT", `/v4/spreadsheets/${spreadsheetId}/values/${encodeA1(targetRange)}`, { range: targetRange, values }, { valueInputOption });
951
+ }
952
+ }
953
+ return {
954
+ updated: prepared.updateData.length,
955
+ appended: prepared.appendData.length,
956
+ newColumns: prepared.newColumns,
957
+ };
920
958
  }
921
- return {
922
- updated: prepared.updateData.length,
923
- appended: prepared.appendData.length,
924
- newColumns: prepared.newColumns,
925
- };
926
959
  // ─── Charts ─────────────────────────────────────────────────────
927
960
  rl.registerAction("chart.add", {
928
961
  access: "write",
929
962
  description: "Embed a chart on a sheet. Minimal signature: type ('COLUMN'|'BAR'|'LINE'|'AREA'|'PIE'|'SCATTER'|'COMBO'), data source (sheet name + A1 range), and target anchor cell. For full control pass a raw `chartSpec` object instead.",
930
- inputSchema: {
931
- spreadsheetId: { type: "string", required: true },
932
- title: { type: "string", required: false },
933
- type: { type: "string", required: false, description: "Basic chart type. Default COLUMN. Ignored when `chartSpec` is provided." },
934
- sourceSheet: { type: "string", required: false, description: "Sheet name holding the source range. Ignored when `chartSpec` is provided." },
935
- sourceRange: { type: "string", required: false, description: "A1 range on `sourceSheet`, e.g. 'A1:D20'. Ignored when `chartSpec` is provided." },
936
- legendPosition: { type: "string", required: false, description: "BOTTOM_LEGEND | TOP_LEGEND | RIGHT_LEGEND | LEFT_LEGEND | NO_LEGEND. Default BOTTOM_LEGEND." },
937
- stackedType: { type: "string", required: false, description: "STACKED | PERCENT_STACKED — for COLUMN/BAR/AREA." },
938
- anchorSheet: { type: "string", required: true, description: "Sheet to place the chart on." },
939
- anchorRow: { type: "number", required: false, default: 0 },
940
- anchorColumn: { type: "number", required: false, default: 0 },
941
- widthPx: { type: "number", required: false, default: 600 },
942
- heightPx: { type: "number", required: false, default: 371 },
943
- chartSpec: { type: "object", required: false, description: "Raw ChartSpec object; bypasses the simple type+source builder." },
944
- },
963
+ inputSchema: t.Object({
964
+ spreadsheetId: Id,
965
+ title: t.Optional(t.String()),
966
+ type: t.Optional(stringEnum([
967
+ "COLUMN",
968
+ "BAR",
969
+ "LINE",
970
+ "AREA",
971
+ "PIE",
972
+ "SCATTER",
973
+ "COMBO",
974
+ ])),
975
+ sourceSheet: t.Optional(sheetReferenceSchema),
976
+ sourceRange: t.Optional(NonEmptyString),
977
+ legendPosition: t.Optional(stringEnum([
978
+ "BOTTOM_LEGEND",
979
+ "TOP_LEGEND",
980
+ "RIGHT_LEGEND",
981
+ "LEFT_LEGEND",
982
+ "NO_LEGEND",
983
+ ])),
984
+ stackedType: t.Optional(stringEnum(["STACKED", "PERCENT_STACKED"])),
985
+ anchorSheet: sheetReferenceSchema,
986
+ anchorRow: t.Optional(NonNegativeInteger),
987
+ anchorColumn: t.Optional(NonNegativeInteger),
988
+ widthPx: t.Optional(PositiveInteger),
989
+ heightPx: t.Optional(PositiveInteger),
990
+ chartSpec: t.Optional(RawGoogleObject),
991
+ }, {
992
+ additionalProperties: false,
993
+ oneOf: [
994
+ {
995
+ required: ["chartSpec"],
996
+ not: {
997
+ anyOf: [
998
+ { required: ["title"] },
999
+ { required: ["type"] },
1000
+ { required: ["sourceSheet"] },
1001
+ { required: ["sourceRange"] },
1002
+ { required: ["legendPosition"] },
1003
+ { required: ["stackedType"] },
1004
+ ],
1005
+ },
1006
+ },
1007
+ {
1008
+ required: ["sourceSheet", "sourceRange"],
1009
+ not: { required: ["chartSpec"] },
1010
+ },
1011
+ ],
1012
+ }),
945
1013
  async execute(input, ctx) {
946
1014
  const p = (input ?? {});
947
1015
  const spreadsheetId = p.spreadsheetId;
@@ -960,14 +1028,41 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
960
1028
  legendPosition: p.legendPosition ?? "BOTTOM_LEGEND",
961
1029
  stackedType: p.stackedType,
962
1030
  headerCount: 1,
963
- domains: [{ domain: { sourceRange: { sources: [{
964
- sheetId: grid.sheetId, startRowIndex: grid.startRowIndex, endRowIndex: grid.endRowIndex,
965
- startColumnIndex: grid.startColumnIndex, endColumnIndex: grid.startColumnIndex + 1,
966
- }] } } }],
967
- series: [{ series: { sourceRange: { sources: [{
968
- sheetId: grid.sheetId, startRowIndex: grid.startRowIndex, endRowIndex: grid.endRowIndex,
969
- startColumnIndex: grid.startColumnIndex + 1, endColumnIndex: grid.endColumnIndex,
970
- }] } }, targetAxis: "LEFT_AXIS" }],
1031
+ domains: [
1032
+ {
1033
+ domain: {
1034
+ sourceRange: {
1035
+ sources: [
1036
+ {
1037
+ sheetId: grid.sheetId,
1038
+ startRowIndex: grid.startRowIndex,
1039
+ endRowIndex: grid.endRowIndex,
1040
+ startColumnIndex: grid.startColumnIndex,
1041
+ endColumnIndex: grid.startColumnIndex + 1,
1042
+ },
1043
+ ],
1044
+ },
1045
+ },
1046
+ },
1047
+ ],
1048
+ series: [
1049
+ {
1050
+ series: {
1051
+ sourceRange: {
1052
+ sources: [
1053
+ {
1054
+ sheetId: grid.sheetId,
1055
+ startRowIndex: grid.startRowIndex,
1056
+ endRowIndex: grid.endRowIndex,
1057
+ startColumnIndex: grid.startColumnIndex + 1,
1058
+ endColumnIndex: grid.endColumnIndex,
1059
+ },
1060
+ ],
1061
+ },
1062
+ },
1063
+ targetAxis: "LEFT_AXIS",
1064
+ },
1065
+ ],
971
1066
  },
972
1067
  };
973
1068
  }
@@ -977,7 +1072,11 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
977
1072
  spec,
978
1073
  position: {
979
1074
  overlayPosition: {
980
- anchorCell: { sheetId: anchorSheetId, rowIndex: p.anchorRow ?? 0, columnIndex: p.anchorColumn ?? 0 },
1075
+ anchorCell: {
1076
+ sheetId: anchorSheetId,
1077
+ rowIndex: p.anchorRow ?? 0,
1078
+ columnIndex: p.anchorColumn ?? 0,
1079
+ },
981
1080
  widthPixels: p.widthPx ?? 600,
982
1081
  heightPixels: p.heightPx ?? 371,
983
1082
  },
@@ -991,12 +1090,12 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
991
1090
  rl.registerAction("chart.update", {
992
1091
  access: "write",
993
1092
  description: "Update an existing chart's spec by chartId.",
994
- inputSchema: {
995
- spreadsheetId: { type: "string", required: true },
996
- chartId: { type: "number", required: true },
997
- chartSpec: { type: "object", required: true },
998
- fields: { type: "string", required: false, default: "*" },
999
- },
1093
+ inputSchema: t.Object({
1094
+ spreadsheetId: Id,
1095
+ chartId: NonNegativeInteger,
1096
+ chartSpec: RawGoogleObject,
1097
+ fields: t.Optional(NonEmptyString),
1098
+ }, { additionalProperties: false }),
1000
1099
  async execute(input, ctx) {
1001
1100
  const p = (input ?? {});
1002
1101
  return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
@@ -1013,10 +1112,10 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1013
1112
  rl.registerAction("chart.delete", {
1014
1113
  access: "write",
1015
1114
  description: "Delete a chart by its embedded-object id.",
1016
- inputSchema: {
1017
- spreadsheetId: { type: "string", required: true },
1018
- chartId: { type: "number", required: true },
1019
- },
1115
+ inputSchema: t.Object({
1116
+ spreadsheetId: Id,
1117
+ chartId: NonNegativeInteger,
1118
+ }, { additionalProperties: false }),
1020
1119
  async execute(input, ctx) {
1021
1120
  const p = (input ?? {});
1022
1121
  return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
@@ -1028,12 +1127,12 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1028
1127
  rl.registerAction("namedRange.add", {
1029
1128
  access: "write",
1030
1129
  description: "Create a named range on a sheet (so formulas can reference it by name).",
1031
- inputSchema: {
1032
- spreadsheetId: { type: "string", required: true },
1033
- name: { type: "string", required: true },
1034
- sheet: { type: "string", required: true },
1035
- range: { type: "string", required: true, description: "A1 range on `sheet`, e.g. 'A1:D20'." },
1036
- },
1130
+ inputSchema: t.Object({
1131
+ spreadsheetId: Id,
1132
+ name: NonEmptyString,
1133
+ sheet: sheetReferenceSchema,
1134
+ range: NonEmptyString,
1135
+ }, { additionalProperties: false }),
1037
1136
  async execute(input, ctx) {
1038
1137
  const p = (input ?? {});
1039
1138
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
@@ -1046,10 +1145,10 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1046
1145
  rl.registerAction("namedRange.delete", {
1047
1146
  access: "write",
1048
1147
  description: "Delete a named range by its id.",
1049
- inputSchema: {
1050
- spreadsheetId: { type: "string", required: true },
1051
- namedRangeId: { type: "string", required: true },
1052
- },
1148
+ inputSchema: t.Object({
1149
+ spreadsheetId: Id,
1150
+ namedRangeId: Id,
1151
+ }, { additionalProperties: false }),
1053
1152
  async execute(input, ctx) {
1054
1153
  const p = (input ?? {});
1055
1154
  return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
@@ -1061,18 +1160,21 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1061
1160
  rl.registerAction("protectedRange.add", {
1062
1161
  access: "write",
1063
1162
  description: "Protect a range from edits. Pass `editorEmails` to restrict who can still edit; without it, only the sheet owner can edit.",
1064
- inputSchema: {
1065
- spreadsheetId: { type: "string", required: true },
1066
- sheet: { type: "string", required: true },
1067
- range: { type: "string", required: false, description: "A1 range. Omit to protect the whole sheet." },
1068
- description: { type: "string", required: false },
1069
- warningOnly: { type: "boolean", required: false, description: "When true, edits are allowed but show a warning." },
1070
- editorEmails: { type: "array", required: false },
1071
- },
1163
+ inputSchema: t.Object({
1164
+ spreadsheetId: Id,
1165
+ sheet: sheetReferenceSchema,
1166
+ range: t.Optional(NonEmptyString),
1167
+ description: t.Optional(t.String()),
1168
+ warningOnly: t.Optional(t.Boolean()),
1169
+ editorEmails: t.Optional(t.Array(NonEmptyString)),
1170
+ }, { additionalProperties: false }),
1072
1171
  async execute(input, ctx) {
1073
1172
  const p = (input ?? {});
1074
1173
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
1075
- const protectedRange = { description: p.description, warningOnly: p.warningOnly };
1174
+ const protectedRange = {
1175
+ description: p.description,
1176
+ warningOnly: p.warningOnly,
1177
+ };
1076
1178
  if (p.range)
1077
1179
  protectedRange.range = a1RangeToGridRange(p.range, sheetId);
1078
1180
  else
@@ -1088,10 +1190,10 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1088
1190
  rl.registerAction("protectedRange.delete", {
1089
1191
  access: "write",
1090
1192
  description: "Delete a protected range by its id.",
1091
- inputSchema: {
1092
- spreadsheetId: { type: "string", required: true },
1093
- protectedRangeId: { type: "number", required: true },
1094
- },
1193
+ inputSchema: t.Object({
1194
+ spreadsheetId: Id,
1195
+ protectedRangeId: NonNegativeInteger,
1196
+ }, { additionalProperties: false }),
1095
1197
  async execute(input, ctx) {
1096
1198
  const p = (input ?? {});
1097
1199
  return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
@@ -1103,21 +1205,37 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1103
1205
  rl.registerAction("conditionalFormat.add", {
1104
1206
  access: "write",
1105
1207
  description: "Add a single-condition conditional-format rule. For complex rules pass a raw `rule` object instead of the simple builder.",
1106
- inputSchema: {
1107
- spreadsheetId: { type: "string", required: true },
1108
- sheet: { type: "string", required: true },
1109
- range: { type: "string", required: true },
1110
- condition: {
1111
- type: "object",
1112
- required: false,
1113
- description: "BooleanCondition, e.g. { type: 'NUMBER_GREATER', values: [{ userEnteredValue: '100' }] }.",
1114
- },
1115
- backgroundColorHex: { type: "string", required: false },
1116
- foregroundColorHex: { type: "string", required: false },
1117
- bold: { type: "boolean", required: false },
1118
- italic: { type: "boolean", required: false },
1119
- rule: { type: "object", required: false, description: "Raw ConditionalFormatRule; bypasses the simple builder." },
1120
- },
1208
+ inputSchema: t.Object({
1209
+ spreadsheetId: Id,
1210
+ sheet: sheetReferenceSchema,
1211
+ range: NonEmptyString,
1212
+ condition: t.Optional(RawGoogleObject),
1213
+ backgroundColorHex: t.Optional(t.String({ pattern: "^#?[0-9a-fA-F]{6}$" })),
1214
+ foregroundColorHex: t.Optional(t.String({ pattern: "^#?[0-9a-fA-F]{6}$" })),
1215
+ bold: t.Optional(t.Boolean()),
1216
+ italic: t.Optional(t.Boolean()),
1217
+ rule: t.Optional(RawGoogleObject),
1218
+ }, {
1219
+ additionalProperties: false,
1220
+ oneOf: [
1221
+ {
1222
+ required: ["rule"],
1223
+ not: {
1224
+ anyOf: [
1225
+ { required: ["condition"] },
1226
+ { required: ["backgroundColorHex"] },
1227
+ { required: ["foregroundColorHex"] },
1228
+ { required: ["bold"] },
1229
+ { required: ["italic"] },
1230
+ ],
1231
+ },
1232
+ },
1233
+ {
1234
+ required: ["condition"],
1235
+ not: { required: ["rule"] },
1236
+ },
1237
+ ],
1238
+ }),
1121
1239
  async execute(input, ctx) {
1122
1240
  const p = (input ?? {});
1123
1241
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
@@ -1139,19 +1257,24 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1139
1257
  ts.italic = p.italic;
1140
1258
  fmt.textFormat = ts;
1141
1259
  }
1142
- rule = { ranges: [grid], booleanRule: { condition: p.condition, format: fmt } };
1260
+ rule = {
1261
+ ranges: [grid],
1262
+ booleanRule: { condition: p.condition, format: fmt },
1263
+ };
1143
1264
  }
1144
- return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [{ addConditionalFormatRule: { rule, index: 0 } }]);
1265
+ return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
1266
+ { addConditionalFormatRule: { rule, index: 0 } },
1267
+ ]);
1145
1268
  },
1146
1269
  });
1147
1270
  rl.registerAction("conditionalFormat.delete", {
1148
1271
  access: "write",
1149
1272
  description: "Delete a conditional-format rule by its index within the sheet's rule list.",
1150
- inputSchema: {
1151
- spreadsheetId: { type: "string", required: true },
1152
- sheet: { type: "string", required: true },
1153
- index: { type: "number", required: true },
1154
- },
1273
+ inputSchema: t.Object({
1274
+ spreadsheetId: Id,
1275
+ sheet: sheetReferenceSchema,
1276
+ index: NonNegativeInteger,
1277
+ }, { additionalProperties: false }),
1155
1278
  async execute(input, ctx) {
1156
1279
  const p = (input ?? {});
1157
1280
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
@@ -1164,17 +1287,52 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1164
1287
  rl.registerAction("dataValidation.set", {
1165
1288
  access: "write",
1166
1289
  description: "Apply a data-validation rule to a range. Pass either `condition` (BooleanCondition) or `oneOfList` (array of values for ONE_OF_LIST). Use `clear: true` to remove validation.",
1167
- inputSchema: {
1168
- spreadsheetId: { type: "string", required: true },
1169
- sheet: { type: "string", required: true },
1170
- range: { type: "string", required: true },
1171
- condition: { type: "object", required: false },
1172
- oneOfList: { type: "array", required: false },
1173
- showCustomUi: { type: "boolean", required: false, default: true },
1174
- strict: { type: "boolean", required: false, default: true },
1175
- inputMessage: { type: "string", required: false },
1176
- clear: { type: "boolean", required: false },
1177
- },
1290
+ inputSchema: t.Object({
1291
+ spreadsheetId: Id,
1292
+ sheet: sheetReferenceSchema,
1293
+ range: NonEmptyString,
1294
+ condition: t.Optional(RawGoogleObject),
1295
+ oneOfList: t.Optional(t.Array(JsonValue, { minItems: 1 })),
1296
+ showCustomUi: t.Optional(t.Boolean()),
1297
+ strict: t.Optional(t.Boolean()),
1298
+ inputMessage: t.Optional(t.String()),
1299
+ clear: t.Optional(t.Boolean()),
1300
+ }, {
1301
+ additionalProperties: false,
1302
+ oneOf: [
1303
+ {
1304
+ required: ["condition"],
1305
+ not: {
1306
+ anyOf: [
1307
+ { required: ["oneOfList"] },
1308
+ { required: ["clear"] },
1309
+ ],
1310
+ },
1311
+ },
1312
+ {
1313
+ required: ["oneOfList"],
1314
+ not: {
1315
+ anyOf: [
1316
+ { required: ["condition"] },
1317
+ { required: ["clear"] },
1318
+ ],
1319
+ },
1320
+ },
1321
+ {
1322
+ properties: { clear: { const: true } },
1323
+ required: ["clear"],
1324
+ not: {
1325
+ anyOf: [
1326
+ { required: ["condition"] },
1327
+ { required: ["oneOfList"] },
1328
+ { required: ["showCustomUi"] },
1329
+ { required: ["strict"] },
1330
+ { required: ["inputMessage"] },
1331
+ ],
1332
+ },
1333
+ },
1334
+ ],
1335
+ }),
1178
1336
  async execute(input, ctx) {
1179
1337
  const p = (input ?? {});
1180
1338
  const sheetId = await resolveSheetId(ctx, p.spreadsheetId, p.sheet);
@@ -1185,12 +1343,19 @@ async function runUpdateOrUpsert(ctx, p, upsert) {
1185
1343
  if (!cond && Array.isArray(p.oneOfList)) {
1186
1344
  cond = {
1187
1345
  type: "ONE_OF_LIST",
1188
- values: p.oneOfList.map((v) => ({ userEnteredValue: String(v) })),
1346
+ values: p.oneOfList.map((v) => ({
1347
+ userEnteredValue: String(v),
1348
+ })),
1189
1349
  };
1190
1350
  }
1191
1351
  if (!cond)
1192
1352
  throw new Error("googleSheets.dataValidation.set: pass condition or oneOfList, or set clear=true");
1193
- rule = { condition: cond, strict: p.strict ?? true, showCustomUi: p.showCustomUi ?? true, inputMessage: p.inputMessage };
1353
+ rule = {
1354
+ condition: cond,
1355
+ strict: p.strict ?? true,
1356
+ showCustomUi: p.showCustomUi ?? true,
1357
+ inputMessage: p.inputMessage,
1358
+ };
1194
1359
  }
1195
1360
  return spreadsheetBatchUpdate(ctx, p.spreadsheetId, [
1196
1361
  { setDataValidation: { range: grid, rule: rule ?? undefined } },