rowbound 1.0.2
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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/adapters/adapter.d.ts +1 -0
- package/dist/adapters/adapter.js +1 -0
- package/dist/adapters/sheets/sheets-adapter.d.ts +66 -0
- package/dist/adapters/sheets/sheets-adapter.js +531 -0
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +397 -0
- package/dist/cli/env.d.ts +3 -0
- package/dist/cli/env.js +103 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +6 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +72 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +212 -0
- package/dist/cli/runs.d.ts +2 -0
- package/dist/cli/runs.js +108 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +108 -0
- package/dist/cli/sync.d.ts +2 -0
- package/dist/cli/sync.js +84 -0
- package/dist/cli/watch.d.ts +2 -0
- package/dist/cli/watch.js +348 -0
- package/dist/core/condition.d.ts +25 -0
- package/dist/core/condition.js +66 -0
- package/dist/core/defaults.d.ts +3 -0
- package/dist/core/defaults.js +7 -0
- package/dist/core/engine.d.ts +50 -0
- package/dist/core/engine.js +234 -0
- package/dist/core/env.d.ts +13 -0
- package/dist/core/env.js +72 -0
- package/dist/core/exec.d.ts +24 -0
- package/dist/core/exec.js +134 -0
- package/dist/core/extractor.d.ts +10 -0
- package/dist/core/extractor.js +33 -0
- package/dist/core/http-client.d.ts +32 -0
- package/dist/core/http-client.js +161 -0
- package/dist/core/rate-limiter.d.ts +25 -0
- package/dist/core/rate-limiter.js +64 -0
- package/dist/core/reconcile.d.ts +24 -0
- package/dist/core/reconcile.js +192 -0
- package/dist/core/run-format.d.ts +39 -0
- package/dist/core/run-format.js +201 -0
- package/dist/core/run-state.d.ts +64 -0
- package/dist/core/run-state.js +141 -0
- package/dist/core/run-tracker.d.ts +15 -0
- package/dist/core/run-tracker.js +57 -0
- package/dist/core/safe-compare.d.ts +8 -0
- package/dist/core/safe-compare.js +19 -0
- package/dist/core/shell-escape.d.ts +7 -0
- package/dist/core/shell-escape.js +9 -0
- package/dist/core/tab-resolver.d.ts +17 -0
- package/dist/core/tab-resolver.js +44 -0
- package/dist/core/template.d.ts +32 -0
- package/dist/core/template.js +82 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.js +2 -0
- package/dist/core/url-guard.d.ts +21 -0
- package/dist/core/url-guard.js +184 -0
- package/dist/core/validator.d.ts +11 -0
- package/dist/core/validator.js +261 -0
- package/dist/core/waterfall.d.ts +26 -0
- package/dist/core/waterfall.js +55 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +943 -0
- package/package.json +67 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Zod schemas for gws CLI response shapes
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/** Response from `gws sheets spreadsheets values get` */
|
|
7
|
+
const ValuesResponseSchema = z.object({
|
|
8
|
+
values: z.array(z.array(z.string())).optional(),
|
|
9
|
+
});
|
|
10
|
+
/** Response from `gws sheets spreadsheets developerMetadata search` */
|
|
11
|
+
const MetadataSearchResponseSchema = z.object({
|
|
12
|
+
matchedDeveloperMetadata: z
|
|
13
|
+
.array(z.object({
|
|
14
|
+
developerMetadata: z.object({
|
|
15
|
+
metadataId: z.number(),
|
|
16
|
+
metadataKey: z.string(),
|
|
17
|
+
metadataValue: z.string(),
|
|
18
|
+
}),
|
|
19
|
+
}))
|
|
20
|
+
.optional(),
|
|
21
|
+
});
|
|
22
|
+
/** Response from `gws sheets spreadsheets get` (sheets.properties) */
|
|
23
|
+
const SheetsPropertiesResponseSchema = z.object({
|
|
24
|
+
sheets: z
|
|
25
|
+
.array(z.object({
|
|
26
|
+
properties: z.object({
|
|
27
|
+
sheetId: z.number(),
|
|
28
|
+
title: z.string(),
|
|
29
|
+
}),
|
|
30
|
+
}))
|
|
31
|
+
.optional(),
|
|
32
|
+
});
|
|
33
|
+
/** Response from `gws sheets spreadsheets get` (namedRanges) */
|
|
34
|
+
const NamedRangesResponseSchema = z.object({
|
|
35
|
+
namedRanges: z
|
|
36
|
+
.array(z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
namedRangeId: z.string(),
|
|
39
|
+
range: z.object({
|
|
40
|
+
sheetId: z.number().optional(),
|
|
41
|
+
startColumnIndex: z.number(),
|
|
42
|
+
endColumnIndex: z.number(),
|
|
43
|
+
}),
|
|
44
|
+
}))
|
|
45
|
+
.optional(),
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Safely parse gws JSON output with a Zod schema.
|
|
49
|
+
* Wraps parse errors with a descriptive message.
|
|
50
|
+
*/
|
|
51
|
+
function parseGwsResponse(output, schema, context) {
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = JSON.parse(output);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`gws returned invalid JSON for ${context}`);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return schema.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
throw new Error(`gws returned unexpected response format for ${context}: ${e instanceof Error ? e.message : String(e)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Convert a 0-indexed column number to a spreadsheet column letter.
|
|
68
|
+
* 0 = A, 1 = B, ..., 25 = Z, 26 = AA, 27 = AB, ...
|
|
69
|
+
*/
|
|
70
|
+
export function columnIndexToLetter(index) {
|
|
71
|
+
let result = "";
|
|
72
|
+
let n = index;
|
|
73
|
+
while (true) {
|
|
74
|
+
result = String.fromCharCode((n % 26) + 65) + result;
|
|
75
|
+
n = Math.floor(n / 26) - 1;
|
|
76
|
+
if (n < 0)
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run the gws CLI with the given arguments.
|
|
83
|
+
* Uses execFile (not exec) to avoid shell injection.
|
|
84
|
+
*/
|
|
85
|
+
export function runGws(args) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
execFile("gws", args, (error, stdout, stderr) => {
|
|
88
|
+
if (error) {
|
|
89
|
+
if (error.code === "ENOENT") {
|
|
90
|
+
reject(new Error("'gws' CLI not found. Rowbound requires the Google Workspace CLI (gws) to interact with Google Sheets.\n" +
|
|
91
|
+
"Install: npm install -g @googleworkspace/cli\n" +
|
|
92
|
+
"Then run: gws auth setup\n" +
|
|
93
|
+
"See: https://github.com/googleworkspace/cli"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
reject(new Error(`gws failed: ${stderr || error.message}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
resolve(stdout);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Google Sheets adapter using the gws CLI tool.
|
|
105
|
+
*/
|
|
106
|
+
export class SheetsAdapter {
|
|
107
|
+
headerCache = new Map();
|
|
108
|
+
headerCacheTimes = new Map();
|
|
109
|
+
headerPending = new Map();
|
|
110
|
+
HEADER_CACHE_TTL_MS = 60_000; // 1 minute
|
|
111
|
+
escapeSheetName(name) {
|
|
112
|
+
return name.replace(/'/g, "''");
|
|
113
|
+
}
|
|
114
|
+
cacheKey(ref) {
|
|
115
|
+
return `${ref.spreadsheetId}:${ref.sheetName || "Sheet1"}`;
|
|
116
|
+
}
|
|
117
|
+
sheetName(ref) {
|
|
118
|
+
return ref.sheetName || "Sheet1";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Look up a column name in the headers and return its letter (A, B, ..., AA, etc.).
|
|
122
|
+
*/
|
|
123
|
+
async columnNameToLetter(ref, columnName) {
|
|
124
|
+
const headers = await this.getHeaders(ref);
|
|
125
|
+
const index = headers.indexOf(columnName);
|
|
126
|
+
if (index === -1) {
|
|
127
|
+
throw new Error(`Column "${columnName}" not found in headers: ${headers.join(", ")}`);
|
|
128
|
+
}
|
|
129
|
+
return columnIndexToLetter(index);
|
|
130
|
+
}
|
|
131
|
+
async readRows(ref, range) {
|
|
132
|
+
const effectiveRange = range || `'${this.escapeSheetName(this.sheetName(ref))}'`;
|
|
133
|
+
const output = await runGws([
|
|
134
|
+
"sheets",
|
|
135
|
+
"spreadsheets",
|
|
136
|
+
"values",
|
|
137
|
+
"get",
|
|
138
|
+
"--params",
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
spreadsheetId: ref.spreadsheetId,
|
|
141
|
+
range: effectiveRange,
|
|
142
|
+
}),
|
|
143
|
+
"--format",
|
|
144
|
+
"json",
|
|
145
|
+
]);
|
|
146
|
+
const result = parseGwsResponse(output, ValuesResponseSchema, "readRows");
|
|
147
|
+
if (!result.values || result.values.length === 0) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const [headerRow, ...dataRows] = result.values;
|
|
151
|
+
const headers = headerRow;
|
|
152
|
+
return dataRows.map((row) => {
|
|
153
|
+
const obj = {};
|
|
154
|
+
for (let i = 0; i < headers.length; i++) {
|
|
155
|
+
obj[headers[i]] = i < row.length ? row[i] : "";
|
|
156
|
+
}
|
|
157
|
+
return obj;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async writeCell(ref, update) {
|
|
161
|
+
await this.writeBatch(ref, [update]);
|
|
162
|
+
}
|
|
163
|
+
async writeBatch(ref, updates) {
|
|
164
|
+
const sheet = this.sheetName(ref);
|
|
165
|
+
const data = await Promise.all(updates.map(async (u) => {
|
|
166
|
+
const colLetter = await this.columnNameToLetter(ref, u.column);
|
|
167
|
+
return {
|
|
168
|
+
range: `'${this.escapeSheetName(sheet)}'!${colLetter}${u.row}`,
|
|
169
|
+
values: [[u.value]],
|
|
170
|
+
};
|
|
171
|
+
}));
|
|
172
|
+
await runGws([
|
|
173
|
+
"sheets",
|
|
174
|
+
"spreadsheets",
|
|
175
|
+
"values",
|
|
176
|
+
"batchUpdate",
|
|
177
|
+
"--params",
|
|
178
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
179
|
+
"--json",
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
valueInputOption: "USER_ENTERED",
|
|
182
|
+
data,
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
async readConfig(ref) {
|
|
187
|
+
let output;
|
|
188
|
+
try {
|
|
189
|
+
output = await runGws([
|
|
190
|
+
"sheets",
|
|
191
|
+
"spreadsheets",
|
|
192
|
+
"developerMetadata",
|
|
193
|
+
"search",
|
|
194
|
+
"--params",
|
|
195
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
196
|
+
"--json",
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
dataFilters: [
|
|
199
|
+
{
|
|
200
|
+
developerMetadataLookup: {
|
|
201
|
+
metadataKey: "rowbound_config",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// If the command fails (e.g., 404-like response), treat as no config
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const result = parseGwsResponse(output, MetadataSearchResponseSchema, "readConfig");
|
|
213
|
+
if (!result.matchedDeveloperMetadata ||
|
|
214
|
+
result.matchedDeveloperMetadata.length === 0) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(result.matchedDeveloperMetadata[0].developerMetadata.metadataValue);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new Error("Rowbound config contains invalid JSON in developer metadata.");
|
|
223
|
+
}
|
|
224
|
+
if (typeof parsed !== "object" ||
|
|
225
|
+
parsed === null ||
|
|
226
|
+
Array.isArray(parsed)) {
|
|
227
|
+
throw new Error("Rowbound config is not a valid object.");
|
|
228
|
+
}
|
|
229
|
+
const cfg = parsed;
|
|
230
|
+
if (typeof cfg.version !== "string" && typeof cfg.version !== "number") {
|
|
231
|
+
throw new Error("Rowbound config is missing required field 'version' (string or number).");
|
|
232
|
+
}
|
|
233
|
+
if (typeof cfg.settings !== "object" ||
|
|
234
|
+
cfg.settings === null ||
|
|
235
|
+
Array.isArray(cfg.settings)) {
|
|
236
|
+
throw new Error("Rowbound config is missing required field 'settings' (object).");
|
|
237
|
+
}
|
|
238
|
+
const hasActions = Array.isArray(cfg.actions);
|
|
239
|
+
const hasTabs = typeof cfg.tabs === "object" &&
|
|
240
|
+
cfg.tabs !== null &&
|
|
241
|
+
!Array.isArray(cfg.tabs);
|
|
242
|
+
if (!hasActions && !hasTabs) {
|
|
243
|
+
throw new Error("Rowbound config must have either 'actions' (array) or 'tabs' (object).");
|
|
244
|
+
}
|
|
245
|
+
return parsed;
|
|
246
|
+
}
|
|
247
|
+
async writeConfig(ref, config) {
|
|
248
|
+
const configJson = JSON.stringify(config);
|
|
249
|
+
// Try to read existing config to get metadataId
|
|
250
|
+
let existingId = null;
|
|
251
|
+
try {
|
|
252
|
+
const output = await runGws([
|
|
253
|
+
"sheets",
|
|
254
|
+
"spreadsheets",
|
|
255
|
+
"developerMetadata",
|
|
256
|
+
"search",
|
|
257
|
+
"--params",
|
|
258
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
259
|
+
"--json",
|
|
260
|
+
JSON.stringify({
|
|
261
|
+
dataFilters: [
|
|
262
|
+
{
|
|
263
|
+
developerMetadataLookup: {
|
|
264
|
+
metadataKey: "rowbound_config",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
}),
|
|
269
|
+
]);
|
|
270
|
+
const result = parseGwsResponse(output, MetadataSearchResponseSchema, "writeConfig (search existing)");
|
|
271
|
+
if (result.matchedDeveloperMetadata &&
|
|
272
|
+
result.matchedDeveloperMetadata.length > 0) {
|
|
273
|
+
existingId =
|
|
274
|
+
result.matchedDeveloperMetadata[0].developerMetadata.metadataId;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// No existing config — will create
|
|
279
|
+
}
|
|
280
|
+
if (existingId !== null) {
|
|
281
|
+
// Update existing metadata
|
|
282
|
+
await runGws([
|
|
283
|
+
"sheets",
|
|
284
|
+
"spreadsheets",
|
|
285
|
+
"batchUpdate",
|
|
286
|
+
"--params",
|
|
287
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
288
|
+
"--json",
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
requests: [
|
|
291
|
+
{
|
|
292
|
+
updateDeveloperMetadata: {
|
|
293
|
+
dataFilters: [
|
|
294
|
+
{
|
|
295
|
+
developerMetadataLookup: {
|
|
296
|
+
metadataId: existingId,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
developerMetadata: {
|
|
301
|
+
metadataValue: configJson,
|
|
302
|
+
},
|
|
303
|
+
fields: "metadataValue",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
}),
|
|
308
|
+
]);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Create new metadata
|
|
312
|
+
await runGws([
|
|
313
|
+
"sheets",
|
|
314
|
+
"spreadsheets",
|
|
315
|
+
"batchUpdate",
|
|
316
|
+
"--params",
|
|
317
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
318
|
+
"--json",
|
|
319
|
+
JSON.stringify({
|
|
320
|
+
requests: [
|
|
321
|
+
{
|
|
322
|
+
createDeveloperMetadata: {
|
|
323
|
+
developerMetadata: {
|
|
324
|
+
metadataKey: "rowbound_config",
|
|
325
|
+
metadataValue: configJson,
|
|
326
|
+
location: { spreadsheet: true },
|
|
327
|
+
visibility: "DOCUMENT",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
}),
|
|
333
|
+
]);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async getHeaders(ref) {
|
|
337
|
+
const key = this.cacheKey(ref);
|
|
338
|
+
const cached = this.headerCache.get(key);
|
|
339
|
+
const cachedTime = this.headerCacheTimes.get(key);
|
|
340
|
+
if (cached &&
|
|
341
|
+
cachedTime &&
|
|
342
|
+
Date.now() - cachedTime < this.HEADER_CACHE_TTL_MS) {
|
|
343
|
+
return cached;
|
|
344
|
+
}
|
|
345
|
+
// Deduplicate concurrent requests for the same headers
|
|
346
|
+
const pending = this.headerPending.get(key);
|
|
347
|
+
if (pending)
|
|
348
|
+
return pending;
|
|
349
|
+
const promise = this.fetchHeaders(ref, key);
|
|
350
|
+
this.headerPending.set(key, promise);
|
|
351
|
+
try {
|
|
352
|
+
return await promise;
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
this.headerPending.delete(key);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Clear the header cache. Useful when headers are known to have changed.
|
|
360
|
+
*/
|
|
361
|
+
clearCache() {
|
|
362
|
+
this.headerCache.clear();
|
|
363
|
+
this.headerCacheTimes.clear();
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* List all sheets (tabs) in the spreadsheet with their GIDs and names.
|
|
367
|
+
*/
|
|
368
|
+
async listSheets(spreadsheetId) {
|
|
369
|
+
const output = await runGws([
|
|
370
|
+
"sheets",
|
|
371
|
+
"spreadsheets",
|
|
372
|
+
"get",
|
|
373
|
+
"--params",
|
|
374
|
+
JSON.stringify({
|
|
375
|
+
spreadsheetId,
|
|
376
|
+
fields: "sheets.properties",
|
|
377
|
+
}),
|
|
378
|
+
"--format",
|
|
379
|
+
"json",
|
|
380
|
+
]);
|
|
381
|
+
const result = parseGwsResponse(output, SheetsPropertiesResponseSchema, "listSheets");
|
|
382
|
+
return (result.sheets ?? []).map((s) => ({
|
|
383
|
+
gid: s.properties.sheetId,
|
|
384
|
+
name: s.properties.title,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get the numeric sheet ID (GID) for a sheet.
|
|
389
|
+
* Needed for named range creation.
|
|
390
|
+
*/
|
|
391
|
+
async getSheetGid(ref) {
|
|
392
|
+
const sheets = await this.listSheets(ref.spreadsheetId);
|
|
393
|
+
const targetName = this.sheetName(ref);
|
|
394
|
+
const sheet = sheets.find((s) => s.name === targetName);
|
|
395
|
+
if (!sheet) {
|
|
396
|
+
throw new Error(`Sheet "${targetName}" not found in spreadsheet ${ref.spreadsheetId}`);
|
|
397
|
+
}
|
|
398
|
+
return sheet.gid;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Create a named range pointing to a specific column.
|
|
402
|
+
* Name format: _rowbound_{actionId}
|
|
403
|
+
* Range: entire column (no row bounds).
|
|
404
|
+
*/
|
|
405
|
+
async createColumnRange(ref, actionId, columnIndex) {
|
|
406
|
+
const sheetGid = await this.getSheetGid(ref);
|
|
407
|
+
await runGws([
|
|
408
|
+
"sheets",
|
|
409
|
+
"spreadsheets",
|
|
410
|
+
"batchUpdate",
|
|
411
|
+
"--params",
|
|
412
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
413
|
+
"--json",
|
|
414
|
+
JSON.stringify({
|
|
415
|
+
requests: [
|
|
416
|
+
{
|
|
417
|
+
addNamedRange: {
|
|
418
|
+
namedRange: {
|
|
419
|
+
name: `_rowbound_${actionId}`,
|
|
420
|
+
range: {
|
|
421
|
+
sheetId: sheetGid,
|
|
422
|
+
startColumnIndex: columnIndex,
|
|
423
|
+
endColumnIndex: columnIndex + 1,
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
}),
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Read all Rowbound named ranges for a sheet.
|
|
434
|
+
* Returns a map of actionId -> column index (0-based).
|
|
435
|
+
* When sheetGid is provided, only returns ranges belonging to that tab.
|
|
436
|
+
*/
|
|
437
|
+
async readColumnRanges(ref, sheetGid) {
|
|
438
|
+
const output = await runGws([
|
|
439
|
+
"sheets",
|
|
440
|
+
"spreadsheets",
|
|
441
|
+
"get",
|
|
442
|
+
"--params",
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
spreadsheetId: ref.spreadsheetId,
|
|
445
|
+
fields: "namedRanges",
|
|
446
|
+
}),
|
|
447
|
+
"--format",
|
|
448
|
+
"json",
|
|
449
|
+
]);
|
|
450
|
+
const result = parseGwsResponse(output, NamedRangesResponseSchema, "readColumnRanges");
|
|
451
|
+
const map = new Map();
|
|
452
|
+
const prefix = "_rowbound_";
|
|
453
|
+
if (result.namedRanges) {
|
|
454
|
+
for (const nr of result.namedRanges) {
|
|
455
|
+
if (nr.name.startsWith(prefix)) {
|
|
456
|
+
// Filter by sheetGid if provided.
|
|
457
|
+
// Google Sheets API omits sheetId when it's 0 (the default tab).
|
|
458
|
+
if (sheetGid !== undefined && (nr.range.sheetId ?? 0) !== sheetGid) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const actionId = nr.name.slice(prefix.length);
|
|
462
|
+
map.set(actionId, nr.range.startColumnIndex);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return map;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Delete a named range by action ID.
|
|
470
|
+
*/
|
|
471
|
+
async deleteColumnRange(ref, actionId) {
|
|
472
|
+
// First read to find the namedRangeId
|
|
473
|
+
const output = await runGws([
|
|
474
|
+
"sheets",
|
|
475
|
+
"spreadsheets",
|
|
476
|
+
"get",
|
|
477
|
+
"--params",
|
|
478
|
+
JSON.stringify({
|
|
479
|
+
spreadsheetId: ref.spreadsheetId,
|
|
480
|
+
fields: "namedRanges",
|
|
481
|
+
}),
|
|
482
|
+
"--format",
|
|
483
|
+
"json",
|
|
484
|
+
]);
|
|
485
|
+
const result = parseGwsResponse(output, NamedRangesResponseSchema, "deleteColumnRange");
|
|
486
|
+
const targetName = `_rowbound_${actionId}`;
|
|
487
|
+
const namedRange = result.namedRanges?.find((nr) => nr.name === targetName);
|
|
488
|
+
if (!namedRange) {
|
|
489
|
+
// No named range to delete -- silently return
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
await runGws([
|
|
493
|
+
"sheets",
|
|
494
|
+
"spreadsheets",
|
|
495
|
+
"batchUpdate",
|
|
496
|
+
"--params",
|
|
497
|
+
JSON.stringify({ spreadsheetId: ref.spreadsheetId }),
|
|
498
|
+
"--json",
|
|
499
|
+
JSON.stringify({
|
|
500
|
+
requests: [
|
|
501
|
+
{
|
|
502
|
+
deleteNamedRange: {
|
|
503
|
+
namedRangeId: namedRange.namedRangeId,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
}),
|
|
508
|
+
]);
|
|
509
|
+
}
|
|
510
|
+
async fetchHeaders(ref, key) {
|
|
511
|
+
const sheet = this.sheetName(ref);
|
|
512
|
+
const output = await runGws([
|
|
513
|
+
"sheets",
|
|
514
|
+
"spreadsheets",
|
|
515
|
+
"values",
|
|
516
|
+
"get",
|
|
517
|
+
"--params",
|
|
518
|
+
JSON.stringify({
|
|
519
|
+
spreadsheetId: ref.spreadsheetId,
|
|
520
|
+
range: `'${this.escapeSheetName(sheet)}'!1:1`,
|
|
521
|
+
}),
|
|
522
|
+
"--format",
|
|
523
|
+
"json",
|
|
524
|
+
]);
|
|
525
|
+
const result = parseGwsResponse(output, ValuesResponseSchema, "getHeaders");
|
|
526
|
+
const headers = result.values && result.values.length > 0 ? result.values[0] : [];
|
|
527
|
+
this.headerCache.set(key, headers);
|
|
528
|
+
this.headerCacheTimes.set(key, Date.now());
|
|
529
|
+
return headers;
|
|
530
|
+
}
|
|
531
|
+
}
|