From c64ad935a8d2a6b864fdcd84d4296e2d625d5b2f Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Fri, 24 Apr 2026 23:39:21 +0300 Subject: [PATCH 01/12] add multi-range support Signed-off-by: Hunter Achieng --- packages/googlesheets/package.json | 3 +- packages/googlesheets/src/Adaptor.js | 42 ++++++++++---- packages/googlesheets/test/index.js | 83 +++++++++++++++++++++++++++- pnpm-lock.yaml | 34 ++++++++++++ 4 files changed, 148 insertions(+), 14 deletions(-) diff --git a/packages/googlesheets/package.json b/packages/googlesheets/package.json index 175b546e54..8d157d0b26 100644 --- a/packages/googlesheets/package.json +++ b/packages/googlesheets/package.json @@ -41,7 +41,8 @@ "chai": "4.3.6", "deep-eql": "4.1.1", "nock": "13.2.9", - "rimraf": "3.0.2" + "rimraf": "3.0.2", + "sinon": "^21.1.2" }, "type": "module", "types": "types/index.d.ts", diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index 9a17e0cb4f..7cf410896e 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -139,6 +139,7 @@ export function appendValues(params, callback = s => s) { /** * Batch update values in a Spreadsheet. * @example + * Update a single range * batchUpdateValues({ * spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', * range: 'Sheet1!A1:E1', @@ -147,13 +148,25 @@ export function appendValues(params, callback = s => s) { * ['Really now!', '$100', '1', '3/20/2016'], * ], * }) + * @example + * Update multiple non-contiguous ranges + * batchUpdateValues({ + * spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', + * data: [ + * { range: 'Sheet1!A1', values: [['value1']] }, + * { range: 'Sheet1!B5', values: [['value2']] }, + * { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] }, + * ], + * valueInputOption: 'RAW', + * }) * @function * @public * @param {Object} params - Data object to add to the spreadsheet. * @param {string} [params.spreadsheetId] The spreadsheet ID. - * @param {string} [params.range] The range of values to update. + * @param {string} [params.range] The range of values to update (single-range form). * @param {string} [params.valueInputOption] (Optional) Value update options. Defaults to 'USER_ENTERED' - * @param {array} [params.values] A 2d array of values to update. + * @param {array} [params.values] A 2d array of values to update (single-range form). + * @param {array} [params.data] An array of ValueRange objects `({ range, values })` for updating multiple ranges at once. * @param {function} callback - (Optional) callback function * @returns {Operation} spreadsheet information */ @@ -166,20 +179,27 @@ export function batchUpdateValues(params, callback = s => s) { range, valueInputOption = 'USER_ENTERED', values, + data, } = resolvedParams; - if (!values || values.length === 0) { - console.log('Warning: empty values array'); - return state; + let rangeData; + + if (data !== undefined) { + if (!data || data.length === 0) { + console.log('Warning: empty data array'); + return state; + } + rangeData = data; + } else { + if (!values || values.length === 0) { + console.log('Warning: empty values array'); + return state; + } + rangeData = [{ range, values }]; } const resource = { - data: [ - { - range, - values, - }, - ], + data: rangeData, valueInputOption, }; try { diff --git a/packages/googlesheets/test/index.js b/packages/googlesheets/test/index.js index 18a84935a7..f5a082be5f 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -1,6 +1,8 @@ import { expect } from 'chai'; +import sinon from 'sinon'; +import { google } from 'googleapis'; -import { execute ,appendValues, batchUpdateValues } from '../src/index.js'; +import { execute, appendValues, batchUpdateValues } from '../src/index.js'; describe('execute', () => { @@ -55,7 +57,7 @@ describe('append', () =>{ }); describe('batchUpdateValues', () =>{ - it('should return early if the values array is undefined or nullish', async() => { + it('should return early if the values array is undefined or nullish', async() => { const state = { data: [], } @@ -67,4 +69,81 @@ describe('batchUpdateValues', () =>{ })(state); expect(result).to.eql(state); }); + + it('should return early if the data array is empty', async() => { + const state = { + data: [], + }; + + const result = await batchUpdateValues({ + spreadsheetId: '123-456-789', + data: [], + })(state); + expect(result).to.eql(state); + }); + + describe('with mocked Google Sheets client', () => { + let sandbox; + let mockBatchUpdate; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockBatchUpdate = sandbox.stub().resolves({ + data: { totalUpdatedCells: 3 }, + }); + + sandbox.stub(google, 'sheets').returns({ + spreadsheets: { + values: { batchUpdate: mockBatchUpdate }, + }, + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send multi-range data directly to the API', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; + const multiRangeData = [ + { range: 'Sheet1!A1', values: [['value1']] }, + { range: 'Sheet1!B5', values: [['value2']] }, + { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] }, + ]; + + await execute( + batchUpdateValues({ + spreadsheetId: '123-456-789', + data: multiRangeData, + valueInputOption: 'RAW', + }) + )(state); + + expect(mockBatchUpdate.calledOnce).to.be.true; + const { resource } = mockBatchUpdate.firstCall.args[0]; + expect(resource.data).to.deep.equal(multiRangeData); + expect(resource.valueInputOption).to.equal('RAW'); + }); + + it('should wrap single range/values into a data array (fallback)', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; + + await execute( + batchUpdateValues({ + spreadsheetId: '123-456-789', + range: 'Sheet1!A1:B2', + values: [['a', 'b'], ['c', 'd']], + valueInputOption: 'USER_ENTERED', + }) + )(state); + + expect(mockBatchUpdate.calledOnce).to.be.true; + const { resource } = mockBatchUpdate.firstCall.args[0]; + expect(resource.data).to.deep.equal([ + { range: 'Sheet1!A1:B2', values: [['a', 'b'], ['c', 'd']] }, + ]); + expect(resource.valueInputOption).to.equal('USER_ENTERED'); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 519efacada..1ed33ca19e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1127,6 +1127,9 @@ importers: rimraf: specifier: 3.0.2 version: 3.0.2 + sinon: + specifier: ^21.1.2 + version: 21.1.2 packages/hive: dependencies: @@ -5228,12 +5231,18 @@ packages: '@sinonjs/fake-timers@15.1.0': resolution: {integrity: sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==} + '@sinonjs/fake-timers@15.3.2': + resolution: {integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==} + '@sinonjs/fake-timers@6.0.1': resolution: {integrity: sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==} '@sinonjs/fake-timers@9.1.2': resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + '@sinonjs/samsam@10.0.2': + resolution: {integrity: sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==} + '@sinonjs/samsam@5.3.1': resolution: {integrity: sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==} @@ -7319,6 +7328,10 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -10901,6 +10914,9 @@ packages: sinon@21.0.1: resolution: {integrity: sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==} + sinon@21.1.2: + resolution: {integrity: sha512-FS6mN+/bx7e2ajpXkEmOcWB6xBzWiuNoAQT18/+a20SS4U7FSYl8Ms7N6VTUxN/1JAjkx7aXp+THMC8xdpp0gA==} + sinon@9.2.3: resolution: {integrity: sha512-m+DyAWvqVHZtjnjX/nuShasykFeiZ+nPuEfD4G3gpvKGkXRhkF/6NSt2qN2FjZhfrcHXFzUzI+NLnk+42fnLEw==} deprecated: 16.1.1 @@ -14456,6 +14472,10 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@15.3.2': + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@6.0.1': dependencies: '@sinonjs/commons': 1.8.6 @@ -14464,6 +14484,11 @@ snapshots: dependencies: '@sinonjs/commons': 1.8.6 + '@sinonjs/samsam@10.0.2': + dependencies: + '@sinonjs/commons': 3.0.1 + type-detect: 4.1.0 + '@sinonjs/samsam@5.3.1': dependencies: '@sinonjs/commons': 1.8.6 @@ -17161,6 +17186,8 @@ snapshots: diff@8.0.3: {} + diff@8.0.4: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -21287,6 +21314,13 @@ snapshots: diff: 8.0.3 supports-color: 7.2.0 + sinon@21.1.2: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.3.2 + '@sinonjs/samsam': 10.0.2 + diff: 8.0.4 + sinon@9.2.3: dependencies: '@sinonjs/commons': 1.8.6 From 2233968f1b8efec8bcd496c059b1521cbb90cf4d Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Sat, 25 Apr 2026 00:04:54 +0300 Subject: [PATCH 02/12] Add changeset Signed-off-by: Hunter Achieng --- .changeset/new-things-change.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-things-change.md diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md new file mode 100644 index 0000000000..9bd690aca9 --- /dev/null +++ b/.changeset/new-things-change.md @@ -0,0 +1,5 @@ +--- +'@openfn/language-googlesheets': minor +--- + +`batchUpdateValues()` now accepts a `data` array of `{ range, values }` objects, enabling multi-range updates in a single API call. The existing `range` + `values` params continue to work unchanged. From 5e89d3c615498a7cf85734c90a55cc7767b98d27 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Sat, 25 Apr 2026 00:11:04 +0300 Subject: [PATCH 03/12] fix docs Signed-off-by: Hunter Achieng --- packages/googlesheets/ast.json | 24 +++++++++++++++++++++--- packages/googlesheets/src/Adaptor.js | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/googlesheets/ast.json b/packages/googlesheets/ast.json index 9d269ab4d1..cce3918056 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -100,7 +100,13 @@ "tags": [ { "title": "example", - "description": "batchUpdateValues({\n spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n range: 'Sheet1!A1:E1',\n values: [\n ['From expression', '$15', '2', '3/15/2016'],\n ['Really now!', '$100', '1', '3/20/2016'],\n ],\n})" + "description": "batchUpdateValues({\n spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n range: 'Sheet1!A1:E1',\n values: [\n ['From expression', '$15', '2', '3/15/2016'],\n ['Really now!', '$100', '1', '3/20/2016'],\n ],\n})", + "caption": "Update a single range" + }, + { + "title": "example", + "description": "batchUpdateValues({\n spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n data: [\n { range: 'Sheet1!A1', values: [['value1']] },\n { range: 'Sheet1!B5', values: [['value2']] },\n { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] },\n ],\n valueInputOption: 'RAW',\n})", + "caption": "Update multiple ranges" }, { "title": "function", @@ -135,7 +141,7 @@ }, { "title": "param", - "description": "The range of values to update.", + "description": "The range of values to update (single-range form).", "type": { "type": "OptionalType", "expression": { @@ -159,7 +165,7 @@ }, { "title": "param", - "description": "A 2d array of values to update.", + "description": "A 2d array of values to update (single-range form).", "type": { "type": "OptionalType", "expression": { @@ -169,6 +175,18 @@ }, "name": "params.values" }, + { + "title": "param", + "description": "An array of ValueRange objects `({ range, values })` for updating multiple ranges at once.", + "type": { + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "array" + } + }, + "name": "params.data" + }, { "title": "param", "description": "(Optional) callback function", diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index 7cf410896e..d6c006dac7 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -149,7 +149,7 @@ export function appendValues(params, callback = s => s) { * ], * }) * @example - * Update multiple non-contiguous ranges + * Update multiple ranges * batchUpdateValues({ * spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', * data: [ From 1066dd31c2348f6f7a3b3cf664a34a5466dd9a82 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Mon, 27 Apr 2026 12:43:30 +0300 Subject: [PATCH 04/12] fix input and remove callback in getValues Signed-off-by: Hunter Achieng --- .changeset/new-things-change.md | 44 +++++++++- packages/googlesheets/src/Adaptor.js | 125 +++++++++++++-------------- packages/googlesheets/test/index.js | 101 ++++++++++++---------- 3 files changed, 156 insertions(+), 114 deletions(-) diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md index 9bd690aca9..d28e10dc50 100644 --- a/.changeset/new-things-change.md +++ b/.changeset/new-things-change.md @@ -1,5 +1,45 @@ --- -'@openfn/language-googlesheets': minor +'@openfn/language-googlesheets': major --- -`batchUpdateValues()` now accepts a `data` array of `{ range, values }` objects, enabling multi-range updates in a single API call. The existing `range` + `values` params continue to work unchanged. +Updated `appendValues()`, `batchUpdateValues()`, and `getValues()` to use positional arguments instead of a single params object. + +### Migration Guide + +**`appendValues`** + +```js +// Before +appendValues({ + spreadsheetId: '1abc...', + range: 'Sheet1!A1:E1', + values: [['a', 'b']], +}); + +// Now +appendValues('1abc...', { range: 'Sheet1!A1:E1', values: [['a', 'b']] }); +``` + +**`batchUpdateValues`** + +```js +// Before +batchUpdateValues({ + spreadsheetId: '1abc...', + range: 'Sheet1!A1', + values: [['a']], + valueInputOption: 'RAW', +}); + +// Now — accepts an array of range/values objects +batchUpdateValues( + '1abc...', + [{ range: 'Sheet1!A1', values: [['a']] }], + { valueInputOption: 'RAW' } +); +``` + +**`getValues`** + +Signature unchanged. Callback parameter has been removed; use `fn()` to transform the response instead. + diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index d6c006dac7..4db780cf54 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -78,26 +78,36 @@ export function execute(...operations) { * https://developers.google.com/sheets/api/samples/writing#append_values * @public * @example - * appendValues({ - * spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * range: 'Sheet1!A1:E1', - * values: [ - * ['From expression', '$15', '2', '3/15/2016'], - * ['Really now!', '$100', '1', '3/20/2016'], - * ], - * }) + * appendValues( + * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', + * { + * range: 'Sheet1!A1:E1', + * values: [ + * ['From expression', '$15', '2', '3/15/2016'], + * ['Really now!', '$100', '1', '3/20/2016'], + * ], + * } + * ) * @function - * @param {Object} params - Data object to add to the spreadsheet. - * @param {string} [params.spreadsheetId] The spreadsheet ID. - * @param {string} [params.range] The range of values to update. - * @param {array} [params.values] A 2d array of values to update. - * @param {function} callback - (Optional) Callback function + * @param {string} spreadsheetId - The spreadsheet ID. + * @param {Object} data - Data to append. + * @param {string} data.range - The range to append to. + * @param {array} data.values - A 2d array of values to append. + * @param {Object} [options] - Optional settings. + * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. + * @param {function} [callback] - Optional callback function. * @returns {Operation} */ -export function appendValues(params, callback = s => s) { +export function appendValues(spreadsheetId, data, options = {}, callback = s => s) { return state => { - const [resolvedParams] = expandReferences(state, params); - const { spreadsheetId, range, values } = resolvedParams; + const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = expandReferences( + state, + spreadsheetId, + data, + options + ); + const { range, values } = resolvedData; + const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; if (!values || values.length === 0) { console.log('Warning: empty values array'); @@ -107,13 +117,13 @@ export function appendValues(params, callback = s => s) { return new Promise((resolve, reject) => { client.spreadsheets.values.append( { - spreadsheetId, + spreadsheetId: resolvedSpreadsheetId, range, - valueInputOption: 'USER_ENTERED', + valueInputOption, resource: { range, majorDimension: 'ROWS', - values: values, + values, }, }, function (err, response) { @@ -140,71 +150,53 @@ export function appendValues(params, callback = s => s) { * Batch update values in a Spreadsheet. * @example * Update a single range - * batchUpdateValues({ - * spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * range: 'Sheet1!A1:E1', - * values: [ - * ['From expression', '$15', '2', '3/15/2016'], - * ['Really now!', '$100', '1', '3/20/2016'], - * ], - * }) + * batchUpdateValues( + * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', + * [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15'], ['Really now!', '$100']] }], + * { valueInputOption: 'RAW' } + * ) * @example - * Update multiple ranges - * batchUpdateValues({ - * spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * data: [ + * Update multiple non-contiguous ranges + * batchUpdateValues( + * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', + * [ * { range: 'Sheet1!A1', values: [['value1']] }, * { range: 'Sheet1!B5', values: [['value2']] }, * { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] }, * ], - * valueInputOption: 'RAW', - * }) + * { valueInputOption: 'RAW' } + * ) * @function * @public - * @param {Object} params - Data object to add to the spreadsheet. - * @param {string} [params.spreadsheetId] The spreadsheet ID. - * @param {string} [params.range] The range of values to update (single-range form). - * @param {string} [params.valueInputOption] (Optional) Value update options. Defaults to 'USER_ENTERED' - * @param {array} [params.values] A 2d array of values to update (single-range form). - * @param {array} [params.data] An array of ValueRange objects `({ range, values })` for updating multiple ranges at once. - * @param {function} callback - (Optional) callback function + * @param {string} spreadsheetId - The spreadsheet ID. + * @param {Array<{range: string, values: array}>} data - Array of range/values objects to update. + * @param {Object} [options] - Optional settings. + * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. + * @param {function} [callback] - Optional callback function. * @returns {Operation} spreadsheet information */ -export function batchUpdateValues(params, callback = s => s) { +export function batchUpdateValues(spreadsheetId, data, options = {}, callback = s => s) { return async state => { - const [resolvedParams] = expandReferences(state, params); - - const { + const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = expandReferences( + state, spreadsheetId, - range, - valueInputOption = 'USER_ENTERED', - values, data, - } = resolvedParams; - - let rangeData; + options + ); + const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; - if (data !== undefined) { - if (!data || data.length === 0) { - console.log('Warning: empty data array'); - return state; - } - rangeData = data; - } else { - if (!values || values.length === 0) { - console.log('Warning: empty values array'); - return state; - } - rangeData = [{ range, values }]; + if (!resolvedData || resolvedData.length === 0) { + console.log('Warning: empty data array'); + return state; } const resource = { - data: rangeData, + data: resolvedData, valueInputOption, }; try { const response = await client.spreadsheets.values.batchUpdate({ - spreadsheetId, + spreadsheetId: resolvedSpreadsheetId, resource, }); console.log('%d cells updated.', response.data.totalUpdatedCells); @@ -224,10 +216,9 @@ export function batchUpdateValues(params, callback = s => s) { * @function * @param {string} spreadsheetId The spreadsheet ID. * @param {string} range The sheet range. - * @param {function} callback - (Optional) callback function * @returns {Operation} spreadsheet information */ -export function getValues(spreadsheetId, range, callback = s => s) { +export function getValues(spreadsheetId, range ) { return async state => { const [resolvedSheetId, resolvedRange] = expandReferences( state, @@ -245,7 +236,7 @@ export function getValues(spreadsheetId, range, callback = s => s) { const nextState = { ...composeNextState(state, response.data), response }; - return callback(nextState); + return nextState; } catch (err) { logError(err); throw err; diff --git a/packages/googlesheets/test/index.js b/packages/googlesheets/test/index.js index f5a082be5f..288514a340 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { google } from 'googleapis'; -import { execute, appendValues, batchUpdateValues } from '../src/index.js'; +import { execute, appendValues, batchUpdateValues, getValues } from '../src/index.js'; describe('execute', () => { @@ -42,43 +42,22 @@ describe('execute', () => { describe('append', () =>{ - it('should return early if the values array is undefined or nullish', async() => { - const state = { - data: [], - } - - const result = await appendValues({ - spreadsheetId: '123-456-789', - range: 'Sheet!A1:E1', - values: state.values, - })(state); + it('should return early if the values array is undefined or nullish', async() => { + const state = { data: [] }; + + const result = await appendValues( + '123-456-789', + { range: 'Sheet!A1:E1', values: state.values }, + )(state); expect(result).to.eql(state); }); }); describe('batchUpdateValues', () =>{ - it('should return early if the values array is undefined or nullish', async() => { - const state = { - data: [], - } - - const result = await batchUpdateValues({ - spreadsheetId: '123-456-789', - range: 'Sheet!A1:E1', - values: state.values, - })(state); - expect(result).to.eql(state); - }); - - it('should return early if the data array is empty', async() => { - const state = { - data: [], - }; + it('should return early if data is empty', async() => { + const state = { data: [] }; - const result = await batchUpdateValues({ - spreadsheetId: '123-456-789', - data: [], - })(state); + const result = await batchUpdateValues('123-456-789', [])(state); expect(result).to.eql(state); }); @@ -104,7 +83,7 @@ describe('batchUpdateValues', () =>{ sandbox.restore(); }); - it('should send multi-range data directly to the API', async () => { + it('should send multi-range data to the API', async () => { const state = { configuration: { access_token: 'mock-token' }, data: {} }; const multiRangeData = [ { range: 'Sheet1!A1', values: [['value1']] }, @@ -113,11 +92,7 @@ describe('batchUpdateValues', () =>{ ]; await execute( - batchUpdateValues({ - spreadsheetId: '123-456-789', - data: multiRangeData, - valueInputOption: 'RAW', - }) + batchUpdateValues('123-456-789', multiRangeData, { valueInputOption: 'RAW' }) )(state); expect(mockBatchUpdate.calledOnce).to.be.true; @@ -126,16 +101,15 @@ describe('batchUpdateValues', () =>{ expect(resource.valueInputOption).to.equal('RAW'); }); - it('should wrap single range/values into a data array (fallback)', async () => { + it('should send a single range entry to the API', async () => { const state = { configuration: { access_token: 'mock-token' }, data: {} }; await execute( - batchUpdateValues({ - spreadsheetId: '123-456-789', - range: 'Sheet1!A1:B2', - values: [['a', 'b'], ['c', 'd']], - valueInputOption: 'USER_ENTERED', - }) + batchUpdateValues( + '123-456-789', + [{ range: 'Sheet1!A1:B2', values: [['a', 'b'], ['c', 'd']] }], + { valueInputOption: 'USER_ENTERED' } + ) )(state); expect(mockBatchUpdate.calledOnce).to.be.true; @@ -147,3 +121,40 @@ describe('batchUpdateValues', () =>{ }); }); }); + +describe('getValues', () => { + let sandbox; + let mockGet; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockGet = sandbox.stub().resolves({ + data: { values: [['a', 'b'], ['c', 'd']] }, + }); + + sandbox.stub(google, 'sheets').returns({ + spreadsheets: { + values: { get: mockGet }, + }, + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('returns state with the values from the API', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; + + const result = await execute( + getValues('123-456-789', 'Sheet1!A1:B2') + )(state); + + expect(mockGet.calledOnce).to.be.true; + const callArgs = mockGet.firstCall.args[0]; + expect(callArgs.spreadsheetId).to.equal('123-456-789'); + expect(callArgs.range).to.equal('Sheet1!A1:B2'); + expect(result.data).to.deep.equal({ values: [['a', 'b'], ['c', 'd']] }); + }); +}); From 49de380967b668a0c352178d2b9ca222ca1becaa Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Mon, 27 Apr 2026 13:12:53 +0300 Subject: [PATCH 05/12] fix comments Signed-off-by: Hunter Achieng --- .changeset/new-things-change.md | 4 +- packages/googlesheets/ast.json | 172 +++++++++++++-------------- packages/googlesheets/src/Adaptor.js | 18 ++- 3 files changed, 88 insertions(+), 106 deletions(-) diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md index d28e10dc50..23a1d2918d 100644 --- a/.changeset/new-things-change.md +++ b/.changeset/new-things-change.md @@ -39,7 +39,5 @@ batchUpdateValues( ); ``` -**`getValues`** - -Signature unchanged. Callback parameter has been removed; use `fn()` to transform the response instead. +Callback parameter has been removed from `appendValues()`, `batchUpdateValues()`, and `getValues()` in favor of a promise-based API. diff --git a/packages/googlesheets/ast.json b/packages/googlesheets/ast.json index cce3918056..c0e03db6a9 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -3,8 +3,9 @@ { "name": "appendValues", "params": [ - "params", - "callback" + "spreadsheetId", + "data", + "options" ], "docs": { "description": "Add an array of rows to the spreadsheet.\nhttps://developers.google.com/sheets/api/samples/writing#append_values", @@ -16,7 +17,7 @@ }, { "title": "example", - "description": "appendValues({\n spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n range: 'Sheet1!A1:E1',\n values: [\n ['From expression', '$15', '2', '3/15/2016'],\n ['Really now!', '$100', '1', '3/20/2016'],\n ],\n})" + "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n {\n range: 'Sheet1!A1:E1',\n values: [\n ['From expression', '$15', '2', '3/15/2016'],\n ['Really now!', '$100', '1', '3/20/2016'],\n ],\n }\n)" }, { "title": "function", @@ -25,57 +26,63 @@ }, { "title": "param", - "description": "Data object to add to the spreadsheet.", + "description": "The spreadsheet ID.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "spreadsheetId" + }, + { + "title": "param", + "description": "Data to append.", "type": { "type": "NameExpression", "name": "Object" }, - "name": "params" + "name": "data" }, { "title": "param", - "description": "The spreadsheet ID.", + "description": "The range to append to.", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "string" - } + "type": "NameExpression", + "name": "string" }, - "name": "params.spreadsheetId" + "name": "data.range" }, { "title": "param", - "description": "The range of values to update.", + "description": "A 2d array of values to append.", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "string" - } + "type": "NameExpression", + "name": "array" }, - "name": "params.range" + "name": "data.values" }, { "title": "param", - "description": "A 2d array of values to update.", + "description": "Optional settings.", "type": { "type": "OptionalType", "expression": { "type": "NameExpression", - "name": "array" + "name": "Object" } }, - "name": "params.values" + "name": "options" }, { "title": "param", - "description": "(Optional) Callback function", + "description": "Defaults to 'USER_ENTERED'.", "type": { - "type": "NameExpression", - "name": "function" + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "string" + } }, - "name": "callback" + "name": "options.valueInputOption" }, { "title": "returns", @@ -92,21 +99,22 @@ { "name": "batchUpdateValues", "params": [ - "params", - "callback" + "spreadsheetId", + "data", + "options" ], "docs": { "description": "Batch update values in a Spreadsheet.", "tags": [ { "title": "example", - "description": "batchUpdateValues({\n spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n range: 'Sheet1!A1:E1',\n values: [\n ['From expression', '$15', '2', '3/15/2016'],\n ['Really now!', '$100', '1', '3/20/2016'],\n ],\n})", + "description": "batchUpdateValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15'], ['Really now!', '$100']] }],\n { valueInputOption: 'RAW' }\n)", "caption": "Update a single range" }, { "title": "example", - "description": "batchUpdateValues({\n spreadsheetId: '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n data: [\n { range: 'Sheet1!A1', values: [['value1']] },\n { range: 'Sheet1!B5', values: [['value2']] },\n { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] },\n ],\n valueInputOption: 'RAW',\n})", - "caption": "Update multiple ranges" + "description": "batchUpdateValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [\n { range: 'Sheet1!A1', values: [['value1']] },\n { range: 'Sheet1!B5', values: [['value2']] },\n { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] },\n ],\n { valueInputOption: 'RAW' }\n)", + "caption": "Update multiple non-contiguous ranges" }, { "title": "function", @@ -120,40 +128,63 @@ }, { "title": "param", - "description": "Data object to add to the spreadsheet.", + "description": "The spreadsheet ID.", "type": { "type": "NameExpression", - "name": "Object" + "name": "string" }, - "name": "params" + "name": "spreadsheetId" }, { "title": "param", - "description": "The spreadsheet ID.", + "description": "Array of range/values objects to update.", "type": { - "type": "OptionalType", + "type": "TypeApplication", "expression": { "type": "NameExpression", - "name": "string" - } - }, - "name": "params.spreadsheetId" - }, - { - "title": "param", - "description": "The range of values to update (single-range form).", + "name": "Array" + }, + "applications": [ + { + "type": "RecordType", + "fields": [ + { + "type": "FieldType", + "key": "range", + "value": { + "type": "NameExpression", + "name": "string" + } + }, + { + "type": "FieldType", + "key": "values", + "value": { + "type": "NameExpression", + "name": "array" + } + } + ] + } + ] + }, + "name": "data" + }, + { + "title": "param", + "description": "Optional settings.", "type": { "type": "OptionalType", "expression": { "type": "NameExpression", - "name": "string" + "name": "Object" } }, - "name": "params.range" + "name": "options" }, { "title": "param", - "description": "(Optional) Value update options. Defaults to 'USER_ENTERED'", + "description": "Defaults to 'USER_ENTERED'.", "type": { "type": "OptionalType", "expression": { @@ -161,40 +192,7 @@ "name": "string" } }, - "name": "params.valueInputOption" - }, - { - "title": "param", - "description": "A 2d array of values to update (single-range form).", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "array" - } - }, - "name": "params.values" - }, - { - "title": "param", - "description": "An array of ValueRange objects `({ range, values })` for updating multiple ranges at once.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "array" - } - }, - "name": "params.data" - }, - { - "title": "param", - "description": "(Optional) callback function", - "type": { - "type": "NameExpression", - "name": "function" - }, - "name": "callback" + "name": "options.valueInputOption" }, { "title": "returns", @@ -212,8 +210,7 @@ "name": "getValues", "params": [ "spreadsheetId", - "range", - "callback" + "range" ], "docs": { "description": "Gets cell values from a Spreadsheet.", @@ -250,15 +247,6 @@ }, "name": "range" }, - { - "title": "param", - "description": "(Optional) callback function", - "type": { - "type": "NameExpression", - "name": "function" - }, - "name": "callback" - }, { "title": "returns", "description": "spreadsheet information", diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index 4db780cf54..2ea8b4e06f 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -95,10 +95,9 @@ export function execute(...operations) { * @param {array} data.values - A 2d array of values to append. * @param {Object} [options] - Optional settings. * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. - * @param {function} [callback] - Optional callback function. * @returns {Operation} */ -export function appendValues(spreadsheetId, data, options = {}, callback = s => s) { +export function appendValues(spreadsheetId, data, options = {}) { return state => { const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = expandReferences( state, @@ -133,12 +132,10 @@ export function appendValues(spreadsheetId, data, options = {}, callback = s => } else { console.log('Success! Here is the response from Google:'); console.log(response.data); - resolve( - callback({ - ...composeNextState(state, response.data), - response, - }) - ); + resolve({ + ...composeNextState(state, response.data), + response, + }); } } ); @@ -172,10 +169,9 @@ export function appendValues(spreadsheetId, data, options = {}, callback = s => * @param {Array<{range: string, values: array}>} data - Array of range/values objects to update. * @param {Object} [options] - Optional settings. * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. - * @param {function} [callback] - Optional callback function. * @returns {Operation} spreadsheet information */ -export function batchUpdateValues(spreadsheetId, data, options = {}, callback = s => s) { +export function batchUpdateValues(spreadsheetId, data, options = {}) { return async state => { const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = expandReferences( state, @@ -200,7 +196,7 @@ export function batchUpdateValues(spreadsheetId, data, options = {}, callback = resource, }); console.log('%d cells updated.', response.data.totalUpdatedCells); - return callback({ ...composeNextState(state, response.data), response }); + return { ...composeNextState(state, response.data), response }; } catch (err) { logError(err); throw err; From fd518f7169e41cbd25e30b00af07034f1f256f0e Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Mon, 27 Apr 2026 13:36:49 +0300 Subject: [PATCH 06/12] fix appendValues body Signed-off-by: Hunter Achieng --- .changeset/new-things-change.md | 7 ++-- packages/googlesheets/ast.json | 52 ++++++++++++++++------------ packages/googlesheets/src/Adaptor.js | 14 ++------ packages/googlesheets/test/index.js | 2 +- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md index 23a1d2918d..8efa76af72 100644 --- a/.changeset/new-things-change.md +++ b/.changeset/new-things-change.md @@ -17,7 +17,10 @@ appendValues({ }); // Now -appendValues('1abc...', { range: 'Sheet1!A1:E1', values: [['a', 'b']] }); +appendValues( + '1abc...', + [{ range: 'Sheet1!A1:E1', values: [['a', 'b']] }], +); ``` **`batchUpdateValues`** @@ -31,7 +34,7 @@ batchUpdateValues({ valueInputOption: 'RAW', }); -// Now — accepts an array of range/values objects +// Now batchUpdateValues( '1abc...', [{ range: 'Sheet1!A1', values: [['a']] }], diff --git a/packages/googlesheets/ast.json b/packages/googlesheets/ast.json index c0e03db6a9..5398df423d 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -17,7 +17,7 @@ }, { "title": "example", - "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n {\n range: 'Sheet1!A1:E1',\n values: [\n ['From expression', '$15', '2', '3/15/2016'],\n ['Really now!', '$100', '1', '3/20/2016'],\n ],\n }\n)" + "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] }]\n)" }, { "title": "function", @@ -35,31 +35,39 @@ }, { "title": "param", - "description": "Data to append.", + "description": "Array containing a single range/values object to append.", "type": { - "type": "NameExpression", - "name": "Object" + "type": "TypeApplication", + "expression": { + "type": "NameExpression", + "name": "Array" + }, + "applications": [ + { + "type": "RecordType", + "fields": [ + { + "type": "FieldType", + "key": "range", + "value": { + "type": "NameExpression", + "name": "string" + } + }, + { + "type": "FieldType", + "key": "values", + "value": { + "type": "NameExpression", + "name": "array" + } + } + ] + } + ] }, "name": "data" }, - { - "title": "param", - "description": "The range to append to.", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "data.range" - }, - { - "title": "param", - "description": "A 2d array of values to append.", - "type": { - "type": "NameExpression", - "name": "array" - }, - "name": "data.values" - }, { "title": "param", "description": "Optional settings.", diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index 2ea8b4e06f..251c30bb3c 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -80,19 +80,11 @@ export function execute(...operations) { * @example * appendValues( * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * { - * range: 'Sheet1!A1:E1', - * values: [ - * ['From expression', '$15', '2', '3/15/2016'], - * ['Really now!', '$100', '1', '3/20/2016'], - * ], - * } + * [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] }] * ) * @function * @param {string} spreadsheetId - The spreadsheet ID. - * @param {Object} data - Data to append. - * @param {string} data.range - The range to append to. - * @param {array} data.values - A 2d array of values to append. + * @param {Array<{range: string, values: array}>} data - Array containing a single range/values object to append. * @param {Object} [options] - Optional settings. * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. * @returns {Operation} @@ -105,7 +97,7 @@ export function appendValues(spreadsheetId, data, options = {}) { data, options ); - const { range, values } = resolvedData; + const { range, values } = resolvedData[0]; const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; if (!values || values.length === 0) { diff --git a/packages/googlesheets/test/index.js b/packages/googlesheets/test/index.js index 288514a340..6eaafa4543 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -47,7 +47,7 @@ describe('append', () =>{ const result = await appendValues( '123-456-789', - { range: 'Sheet!A1:E1', values: state.values }, + [{ range: 'Sheet!A1:E1', values: state.values }], )(state); expect(result).to.eql(state); }); From 8248c1deef2f3994692c8a0311045bc7b0e0bfe6 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Mon, 27 Apr 2026 16:18:49 +0300 Subject: [PATCH 07/12] fix: tests Signed-off-by: Hunter Achieng --- packages/googlesheets/test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/googlesheets/test/index.js b/packages/googlesheets/test/index.js index 6eaafa4543..6d365a340f 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -47,7 +47,7 @@ describe('append', () =>{ const result = await appendValues( '123-456-789', - [{ range: 'Sheet!A1:E1', values: state.values }], + [{ range: 'Sheet!A1:E1', values: null }], )(state); expect(result).to.eql(state); }); From af3b8cfeb8c333273823a02aa2c1e786ec256119 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Mon, 27 Apr 2026 21:08:53 +0300 Subject: [PATCH 08/12] fix append values body Signed-off-by: Hunter Achieng --- .changeset/new-things-change.md | 2 +- packages/googlesheets/src/Adaptor.js | 6 +++--- packages/googlesheets/test/index.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md index 8efa76af72..d7958ec145 100644 --- a/.changeset/new-things-change.md +++ b/.changeset/new-things-change.md @@ -19,7 +19,7 @@ appendValues({ // Now appendValues( '1abc...', - [{ range: 'Sheet1!A1:E1', values: [['a', 'b']] }], + { range: 'Sheet1!A1:E1', values: [['a', 'b']] }, ); ``` diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index 251c30bb3c..a5554e3cfb 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -80,11 +80,11 @@ export function execute(...operations) { * @example * appendValues( * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] }] + * { range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] } * ) * @function * @param {string} spreadsheetId - The spreadsheet ID. - * @param {Array<{range: string, values: array}>} data - Array containing a single range/values object to append. + * @param {{range: string, values: array}} data - A single range/values object to append. * @param {Object} [options] - Optional settings. * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. * @returns {Operation} @@ -97,7 +97,7 @@ export function appendValues(spreadsheetId, data, options = {}) { data, options ); - const { range, values } = resolvedData[0]; + const { range, values } = resolvedData; const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; if (!values || values.length === 0) { diff --git a/packages/googlesheets/test/index.js b/packages/googlesheets/test/index.js index 6d365a340f..8c6cfa4008 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -47,7 +47,7 @@ describe('append', () =>{ const result = await appendValues( '123-456-789', - [{ range: 'Sheet!A1:E1', values: null }], + { range: 'Sheet!A1:E1', values: null }, )(state); expect(result).to.eql(state); }); From ace54dde87a365839a1d43d9c07317c02ebb003c Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Mon, 27 Apr 2026 21:17:28 +0300 Subject: [PATCH 09/12] fix ast Signed-off-by: Hunter Achieng --- packages/googlesheets/ast.json | 45 ++++++++++++++-------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/packages/googlesheets/ast.json b/packages/googlesheets/ast.json index 5398df423d..8baf225fa2 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -17,7 +17,7 @@ }, { "title": "example", - "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] }]\n)" + "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n { range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] }\n)" }, { "title": "function", @@ -35,34 +35,25 @@ }, { "title": "param", - "description": "Array containing a single range/values object to append.", + "description": "A single range/values object to append.", "type": { - "type": "TypeApplication", - "expression": { - "type": "NameExpression", - "name": "Array" - }, - "applications": [ + "type": "RecordType", + "fields": [ { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "range", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "values", - "value": { - "type": "NameExpression", - "name": "array" - } - } - ] + "type": "FieldType", + "key": "range", + "value": { + "type": "NameExpression", + "name": "string" + } + }, + { + "type": "FieldType", + "key": "values", + "value": { + "type": "NameExpression", + "name": "array" + } } ] }, From ae5d435d9934201439288615d1ebddb678700f56 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Thu, 28 May 2026 16:06:25 +0300 Subject: [PATCH 10/12] feat: update docs Signed-off-by: Hunter Achieng --- packages/googlesheets/src/Adaptor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index a5554e3cfb..a06d08fd54 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -74,7 +74,7 @@ export function execute(...operations) { } /** - * Add an array of rows to the spreadsheet. + * Append one or more rows to a spreadsheet range. * https://developers.google.com/sheets/api/samples/writing#append_values * @public * @example @@ -145,7 +145,7 @@ export function appendValues(spreadsheetId, data, options = {}) { * { valueInputOption: 'RAW' } * ) * @example - * Update multiple non-contiguous ranges + * Update multiple separate ranges * batchUpdateValues( * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', * [ From ac836aaddba5312f46d8db070d769560e5c33405 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Thu, 28 May 2026 16:53:08 +0300 Subject: [PATCH 11/12] feat: build ast Signed-off-by: Hunter Achieng --- packages/googlesheets/ast.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/googlesheets/ast.json b/packages/googlesheets/ast.json index 8baf225fa2..6f432d7a71 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -8,7 +8,7 @@ "options" ], "docs": { - "description": "Add an array of rows to the spreadsheet.\nhttps://developers.google.com/sheets/api/samples/writing#append_values", + "description": "Append one or more rows to a spreadsheet range.\nhttps://developers.google.com/sheets/api/samples/writing#append_values", "tags": [ { "title": "public", @@ -113,7 +113,7 @@ { "title": "example", "description": "batchUpdateValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [\n { range: 'Sheet1!A1', values: [['value1']] },\n { range: 'Sheet1!B5', values: [['value2']] },\n { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] },\n ],\n { valueInputOption: 'RAW' }\n)", - "caption": "Update multiple non-contiguous ranges" + "caption": "Update multiple separate ranges" }, { "title": "function", From 84a8311fda6f75208616acbaff1720799fbf0d36 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 17 Jun 2026 16:42:53 +0300 Subject: [PATCH 12/12] feat: update appendValues signature to use positional range and values args Co-Authored-By: Claude Sonnet 4.6 --- .changeset/new-things-change.md | 3 +- packages/googlesheets/ast.json | 44 +++---- packages/googlesheets/src/Adaptor.js | 52 ++++----- packages/googlesheets/test/index.js | 168 ++++++++++++--------------- 4 files changed, 113 insertions(+), 154 deletions(-) diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md index d7958ec145..ee1c3b8abf 100644 --- a/.changeset/new-things-change.md +++ b/.changeset/new-things-change.md @@ -19,7 +19,8 @@ appendValues({ // Now appendValues( '1abc...', - { range: 'Sheet1!A1:E1', values: [['a', 'b']] }, + 'Sheet1!A1:E1', + [['a', 'b']], ); ``` diff --git a/packages/googlesheets/ast.json b/packages/googlesheets/ast.json index 6f432d7a71..0ba36a1476 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -4,7 +4,8 @@ "name": "appendValues", "params": [ "spreadsheetId", - "data", + "range", + "values", "options" ], "docs": { @@ -17,7 +18,7 @@ }, { "title": "example", - "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n { range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] }\n)" + "description": "appendValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n 'Sheet1!A1:E1',\n [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']]\n)" }, { "title": "function", @@ -35,29 +36,21 @@ }, { "title": "param", - "description": "A single range/values object to append.", + "description": "The sheet range.", "type": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "range", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "values", - "value": { - "type": "NameExpression", - "name": "array" - } - } - ] + "type": "NameExpression", + "name": "string" }, - "name": "data" + "name": "range" + }, + { + "title": "param", + "description": "The values to append.", + "type": { + "type": "NameExpression", + "name": "array" + }, + "name": "values" }, { "title": "param", @@ -105,11 +98,6 @@ "docs": { "description": "Batch update values in a Spreadsheet.", "tags": [ - { - "title": "example", - "description": "batchUpdateValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15'], ['Really now!', '$100']] }],\n { valueInputOption: 'RAW' }\n)", - "caption": "Update a single range" - }, { "title": "example", "description": "batchUpdateValues(\n '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos',\n [\n { range: 'Sheet1!A1', values: [['value1']] },\n { range: 'Sheet1!B5', values: [['value2']] },\n { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] },\n ],\n { valueInputOption: 'RAW' }\n)", diff --git a/packages/googlesheets/src/Adaptor.js b/packages/googlesheets/src/Adaptor.js index a06d08fd54..1e4d70d304 100644 --- a/packages/googlesheets/src/Adaptor.js +++ b/packages/googlesheets/src/Adaptor.js @@ -64,7 +64,7 @@ export function execute(...operations) { return commonExecute( createConnection, ...operations, - removeConnection + removeConnection, )({ ...initialState, ...state, @@ -80,27 +80,28 @@ export function execute(...operations) { * @example * appendValues( * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * { range: 'Sheet1!A1:E1', values: [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] } + * 'Sheet1!A1:E1', + * [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']] * ) * @function * @param {string} spreadsheetId - The spreadsheet ID. - * @param {{range: string, values: array}} data - A single range/values object to append. + * @param {string} range - The sheet range. + * @param {array} values - The values to append. * @param {Object} [options] - Optional settings. * @param {string} [options.valueInputOption] - Defaults to 'USER_ENTERED'. * @returns {Operation} */ -export function appendValues(spreadsheetId, data, options = {}) { +export function appendValues(spreadsheetId, range, values, options = {}) { return state => { - const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = expandReferences( - state, - spreadsheetId, - data, - options - ); - const { range, values } = resolvedData; + const [ + resolvedSpreadsheetId, + resolvedRange, + resolvedValues, + resolvedOptions, + ] = expandReferences(state, spreadsheetId, range, values, options); const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; - if (!values || values.length === 0) { + if (!resolvedValues || resolvedValues.length === 0) { console.log('Warning: empty values array'); return state; } @@ -109,12 +110,12 @@ export function appendValues(spreadsheetId, data, options = {}) { client.spreadsheets.values.append( { spreadsheetId: resolvedSpreadsheetId, - range, + range: resolvedRange, valueInputOption, resource: { - range, + range: resolvedRange, majorDimension: 'ROWS', - values, + values: resolvedValues, }, }, function (err, response) { @@ -129,7 +130,7 @@ export function appendValues(spreadsheetId, data, options = {}) { response, }); } - } + }, ); }); }; @@ -138,13 +139,6 @@ export function appendValues(spreadsheetId, data, options = {}) { /** * Batch update values in a Spreadsheet. * @example - * Update a single range - * batchUpdateValues( - * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', - * [{ range: 'Sheet1!A1:E1', values: [['From expression', '$15'], ['Really now!', '$100']] }], - * { valueInputOption: 'RAW' } - * ) - * @example * Update multiple separate ranges * batchUpdateValues( * '1O-a4_RgPF_p8W3I6b5M9wobA3-CBW8hLClZfUik5sos', @@ -165,12 +159,8 @@ export function appendValues(spreadsheetId, data, options = {}) { */ export function batchUpdateValues(spreadsheetId, data, options = {}) { return async state => { - const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = expandReferences( - state, - spreadsheetId, - data, - options - ); + const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = + expandReferences(state, spreadsheetId, data, options); const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; if (!resolvedData || resolvedData.length === 0) { @@ -206,12 +196,12 @@ export function batchUpdateValues(spreadsheetId, data, options = {}) { * @param {string} range The sheet range. * @returns {Operation} spreadsheet information */ -export function getValues(spreadsheetId, range ) { +export function getValues(spreadsheetId, range) { return async state => { const [resolvedSheetId, resolvedRange] = expandReferences( state, spreadsheetId, - range + range, ); try { diff --git a/packages/googlesheets/test/index.js b/packages/googlesheets/test/index.js index 8c6cfa4008..de377fd9cc 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -4,121 +4,101 @@ import { google } from 'googleapis'; import { execute, appendValues, batchUpdateValues, getValues } from '../src/index.js'; +describe('appendValues', () => { + let sandbox; + let mockAppend; -describe('execute', () => { - it('executes each operation in sequence', done => { - const state = { configuration: {}, data: {} }; - let operations = [ - state => { - return { counter: 1 }; - }, - state => { - return { counter: 2 }; - }, - state => { - return { counter: 3 }; - }, - ]; - - execute(...operations)(state) - .then(finalState => { - expect(finalState).to.eql({ counter: 3 }); - }) - .then(done) - .catch(done); - }); + beforeEach(() => { + sandbox = sinon.createSandbox(); - it('assigns references, data to the initialState', () => { - const state = { configuration: {}, data: {} }; + mockAppend = sandbox.stub().callsArgWith(1, null, { + data: { updates: { updatedCells: 4 } }, + }); - execute()(state).then(finalState => { - expect(finalState).to.eql({ - references: [], - data: null, - }); + sandbox.stub(google, 'sheets').returns({ + spreadsheets: { + values: { append: mockAppend }, + }, }); }); -}); + afterEach(() => { + sandbox.restore(); + }); -describe('append', () =>{ - it('should return early if the values array is undefined or nullish', async() => { - const state = { data: [] }; + it('appends rows to the given range', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; - const result = await appendValues( - '123-456-789', - { range: 'Sheet!A1:E1', values: null }, + const result = await execute( + appendValues('123-456-789', 'Sheet1!A1:B1', [['a', 'b'], ['c', 'd']]) )(state); - expect(result).to.eql(state); + + expect(mockAppend.calledOnce).to.be.true; + const callArgs = mockAppend.firstCall.args[0]; + expect(callArgs.spreadsheetId).to.equal('123-456-789'); + expect(callArgs.range).to.equal('Sheet1!A1:B1'); + expect(callArgs.resource.values).to.deep.equal([['a', 'b'], ['c', 'd']]); + expect(result.data).to.deep.equal({ updates: { updatedCells: 4 } }); }); }); -describe('batchUpdateValues', () =>{ - it('should return early if data is empty', async() => { - const state = { data: [] }; +describe('batchUpdateValues', () => { + let sandbox; + let mockBatchUpdate; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockBatchUpdate = sandbox.stub().resolves({ + data: { totalUpdatedCells: 3 }, + }); - const result = await batchUpdateValues('123-456-789', [])(state); - expect(result).to.eql(state); + sandbox.stub(google, 'sheets').returns({ + spreadsheets: { + values: { batchUpdate: mockBatchUpdate }, + }, + }); }); - describe('with mocked Google Sheets client', () => { - let sandbox; - let mockBatchUpdate; + afterEach(() => { + sandbox.restore(); + }); - beforeEach(() => { - sandbox = sinon.createSandbox(); + it('sends multi-range data to the API', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; + const multiRangeData = [ + { range: 'Sheet1!A1', values: [['value1']] }, + { range: 'Sheet1!B5', values: [['value2']] }, + { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] }, + ]; - mockBatchUpdate = sandbox.stub().resolves({ - data: { totalUpdatedCells: 3 }, - }); + await execute( + batchUpdateValues('123-456-789', multiRangeData, { valueInputOption: 'RAW' }) + )(state); - sandbox.stub(google, 'sheets').returns({ - spreadsheets: { - values: { batchUpdate: mockBatchUpdate }, - }, - }); - }); + expect(mockBatchUpdate.calledOnce).to.be.true; + const { resource } = mockBatchUpdate.firstCall.args[0]; + expect(resource.data).to.deep.equal(multiRangeData); + expect(resource.valueInputOption).to.equal('RAW'); + }); - afterEach(() => { - sandbox.restore(); - }); + it('sends a single range entry to the API', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; - it('should send multi-range data to the API', async () => { - const state = { configuration: { access_token: 'mock-token' }, data: {} }; - const multiRangeData = [ - { range: 'Sheet1!A1', values: [['value1']] }, - { range: 'Sheet1!B5', values: [['value2']] }, - { range: 'Sheet1!D10:E11', values: [['a', 'b'], ['c', 'd']] }, - ]; - - await execute( - batchUpdateValues('123-456-789', multiRangeData, { valueInputOption: 'RAW' }) - )(state); - - expect(mockBatchUpdate.calledOnce).to.be.true; - const { resource } = mockBatchUpdate.firstCall.args[0]; - expect(resource.data).to.deep.equal(multiRangeData); - expect(resource.valueInputOption).to.equal('RAW'); - }); + await execute( + batchUpdateValues( + '123-456-789', + [{ range: 'Sheet1!A1:B2', values: [['a', 'b'], ['c', 'd']] }], + { valueInputOption: 'USER_ENTERED' } + ) + )(state); - it('should send a single range entry to the API', async () => { - const state = { configuration: { access_token: 'mock-token' }, data: {} }; - - await execute( - batchUpdateValues( - '123-456-789', - [{ range: 'Sheet1!A1:B2', values: [['a', 'b'], ['c', 'd']] }], - { valueInputOption: 'USER_ENTERED' } - ) - )(state); - - expect(mockBatchUpdate.calledOnce).to.be.true; - const { resource } = mockBatchUpdate.firstCall.args[0]; - expect(resource.data).to.deep.equal([ - { range: 'Sheet1!A1:B2', values: [['a', 'b'], ['c', 'd']] }, - ]); - expect(resource.valueInputOption).to.equal('USER_ENTERED'); - }); + expect(mockBatchUpdate.calledOnce).to.be.true; + const { resource } = mockBatchUpdate.firstCall.args[0]; + expect(resource.data).to.deep.equal([ + { range: 'Sheet1!A1:B2', values: [['a', 'b'], ['c', 'd']] }, + ]); + expect(resource.valueInputOption).to.equal('USER_ENTERED'); }); });