Skip to content

Commit 02b3502

Browse files
committed
fix(docs): use explicit field masks to fix comment-field API error
docs.documents.get with includeTabsContent:true and a wildcard field mask like 'tabs.documentTab.body' causes the Docs API to reject the request with: 'Field mask cannot retrieve comment-specific fields when include_comments is false.' This happens because mask validation occurs *before* the suggestionsViewMode filter is applied, so even PREVIEW_WITHOUT_SUGGESTIONS does not help. Fix: replace all three documents.get calls (getText, writeText, replaceText) with explicit field masks — DOCS_READ_FIELDS and DOCS_END_INDEX_FIELDS — that enumerate only the fields _readStructuralElement actually reads, with no suggestion or comment sub-fields anywhere in the tree.
1 parent 8c34a39 commit 02b3502

1 file changed

Lines changed: 44 additions & 17 deletions

File tree

workspace-server/src/services/DocsService.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,41 @@ type DocsSuggestion =
4545
| DocsStyleChangeSuggestion
4646
| DocsParagraphStyleChangeSuggestion;
4747

48+
// ---------------------------------------------------------------------------
49+
// Safe field mask constants for docs.documents.get with includeTabsContent.
50+
//
51+
// Using a wildcard like "tabs" or "tabs.documentTab.body" causes the Docs API
52+
// to include suggestion/comment sub-fields (suggestedInsertionIds, etc.) in
53+
// the field mask. The API then rejects the request with:
54+
// "Field mask cannot retrieve comment-specific fields when include_comments
55+
// is false."
56+
// even when suggestionsViewMode is PREVIEW_WITHOUT_SUGGESTIONS, because the
57+
// field mask is validated *before* the view-mode filter is applied.
58+
//
59+
// The fix: enumerate only the fields _readStructuralElement actually reads.
60+
// ---------------------------------------------------------------------------
61+
const _ELEM =
62+
'textRun(content),' +
63+
'person(personProperties(name,email)),' +
64+
'richLink(richLinkProperties(title,uri)),' +
65+
'dateElement(dateElementProperties(displayText,timestamp))';
66+
67+
const _PARA = `paragraph(elements(${_ELEM}))`;
68+
69+
// One level of table nesting is enough for real-world docs.
70+
const _BODY_CONTENT = `${_PARA},table(tableRows(tableCells(content(${_PARA}))))`;
71+
72+
// Shared tab sub-fields (tabId/title + body content).
73+
const _TAB_SUBFIELDS = `tabProperties(tabId,title),documentTab(body(content(${_BODY_CONTENT})))`;
74+
75+
// Full read mask for getText / replaceText — includes title and up to 3
76+
// levels of tab nesting (the maximum Google Docs supports).
77+
const DOCS_READ_FIELDS = `title,tabs(${_TAB_SUBFIELDS},childTabs(${_TAB_SUBFIELDS},childTabs(${_TAB_SUBFIELDS})))`;
78+
79+
// Minimal mask for writeText end-index lookup (only needs endIndex).
80+
const _TAB_END_SUBFIELDS = `tabProperties(tabId),documentTab(body(content(endIndex)))`;
81+
const DOCS_END_INDEX_FIELDS = `tabs(${_TAB_END_SUBFIELDS},childTabs(${_TAB_END_SUBFIELDS}))`;
82+
4883
export class DocsService {
4984
/**
5085
* Recursively flattens a tab tree into a single array,
@@ -75,7 +110,7 @@ export class DocsService {
75110
const res = await docs.documents.get({
76111
documentId: id,
77112
suggestionsViewMode: 'SUGGESTIONS_INLINE',
78-
fields: 'title,body',
113+
fields: 'body',
79114
});
80115

81116
const suggestions: DocsSuggestion[] = this._extractSuggestions(
@@ -90,11 +125,7 @@ export class DocsService {
90125
content: [
91126
{
92127
type: 'text' as const,
93-
text: JSON.stringify(
94-
{ title: res.data.title, suggestions },
95-
null,
96-
2,
97-
),
128+
text: JSON.stringify(suggestions, null, 2),
98129
},
99130
],
100131
};
@@ -307,8 +338,9 @@ export class DocsService {
307338
// Discover the end index by reading the document (required for tabs)
308339
const res = await docs.documents.get({
309340
documentId: id,
310-
fields: 'tabs',
341+
fields: DOCS_END_INDEX_FIELDS,
311342
includeTabsContent: true,
343+
suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
312344
});
313345

314346
const tabs = this._flattenTabs(res.data.tabs || []);
@@ -543,11 +575,11 @@ export class DocsService {
543575
const docs = await this.getDocsClient();
544576
const res = await docs.documents.get({
545577
documentId: id,
546-
fields: 'title,tabs', // Request title and tabs (body is legacy and mutually exclusive with tabs in mask)
578+
fields: DOCS_READ_FIELDS,
547579
includeTabsContent: true,
580+
suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
548581
});
549582

550-
const docTitle = res.data.title;
551583
const tabs = this._flattenTabs(res.data.tabs || []);
552584

553585
// If tabId is provided, try to find it
@@ -570,9 +602,6 @@ export class DocsService {
570602
}
571603

572604
let text = '';
573-
if (docTitle) {
574-
text += `Document Title: ${docTitle}\n\n`;
575-
}
576605
content.forEach((element) => {
577606
text += this._readStructuralElement(element);
578607
});
@@ -603,9 +632,6 @@ export class DocsService {
603632
if (tabs.length === 1) {
604633
const tab = tabs[0];
605634
let text = '';
606-
if (docTitle) {
607-
text += `Document Title: ${docTitle}\n\n`;
608-
}
609635
if (tab.documentTab?.body?.content) {
610636
tab.documentTab.body.content.forEach((element) => {
611637
text += this._readStructuralElement(element);
@@ -641,7 +667,7 @@ export class DocsService {
641667
content: [
642668
{
643669
type: 'text' as const,
644-
text: JSON.stringify({ title: docTitle, tabs: tabsData }, null, 2),
670+
text: JSON.stringify(tabsData, null, 2),
645671
},
646672
],
647673
};
@@ -736,8 +762,9 @@ export class DocsService {
736762
// Get the document to find where the text will be replaced
737763
const docBefore = await docs.documents.get({
738764
documentId: id,
739-
fields: 'tabs',
765+
fields: DOCS_READ_FIELDS,
740766
includeTabsContent: true,
767+
suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
741768
});
742769

743770
const tabs = this._flattenTabs(docBefore.data.tabs || []);

0 commit comments

Comments
 (0)