diff --git a/.changeset/new-things-change.md b/.changeset/new-things-change.md new file mode 100644 index 0000000000..ee1c3b8abf --- /dev/null +++ b/.changeset/new-things-change.md @@ -0,0 +1,47 @@ +--- +'@openfn/language-googlesheets': major +--- + +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...', + 'Sheet1!A1:E1', + [['a', 'b']], +); +``` + +**`batchUpdateValues`** + +```js +// Before +batchUpdateValues({ + spreadsheetId: '1abc...', + range: 'Sheet1!A1', + values: [['a']], + valueInputOption: 'RAW', +}); + +// Now +batchUpdateValues( + '1abc...', + [{ range: 'Sheet1!A1', values: [['a']] }], + { valueInputOption: 'RAW' } +); +``` + +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 9d269ab4d1..0ba36a1476 100644 --- a/packages/googlesheets/ast.json +++ b/packages/googlesheets/ast.json @@ -3,11 +3,13 @@ { "name": "appendValues", "params": [ - "params", - "callback" + "spreadsheetId", + "range", + "values", + "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", @@ -16,7 +18,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 'Sheet1!A1:E1',\n [['From expression', '$15', '2', '3/15/2016'], ['Really now!', '$100', '1', '3/20/2016']]\n)" }, { "title": "function", @@ -25,57 +27,54 @@ }, { "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": "The sheet range.", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "string" - } + "type": "NameExpression", + "name": "string" }, - "name": "params.spreadsheetId" + "name": "range" }, { "title": "param", - "description": "The range of values to update.", + "description": "The values to append.", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "string" - } + "type": "NameExpression", + "name": "array" }, - "name": "params.range" + "name": "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,15 +91,17 @@ { "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 [\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 separate ranges" }, { "title": "function", @@ -114,40 +115,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.", + "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": { @@ -155,28 +179,7 @@ "name": "string" } }, - "name": "params.valueInputOption" - }, - { - "title": "param", - "description": "A 2d array of values to update.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "array" - } - }, - "name": "params.values" - }, - { - "title": "param", - "description": "(Optional) callback function", - "type": { - "type": "NameExpression", - "name": "function" - }, - "name": "callback" + "name": "options.valueInputOption" }, { "title": "returns", @@ -194,8 +197,7 @@ "name": "getValues", "params": [ "spreadsheetId", - "range", - "callback" + "range" ], "docs": { "description": "Gets cell values from a Spreadsheet.", @@ -232,15 +234,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/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..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, @@ -74,32 +74,34 @@ 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 - * 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', + * 'Sheet1!A1:E1', + * [['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 {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(params, callback = s => s) { +export function appendValues(spreadsheetId, range, values, options = {}) { return state => { - const [resolvedParams] = expandReferences(state, params); - const { spreadsheetId, range, values } = resolvedParams; - - if (!values || values.length === 0) { + const [ + resolvedSpreadsheetId, + resolvedRange, + resolvedValues, + resolvedOptions, + ] = expandReferences(state, spreadsheetId, range, values, options); + const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; + + if (!resolvedValues || resolvedValues.length === 0) { console.log('Warning: empty values array'); return state; } @@ -107,13 +109,13 @@ export function appendValues(params, callback = s => s) { return new Promise((resolve, reject) => { client.spreadsheets.values.append( { - spreadsheetId, - range, - valueInputOption: 'USER_ENTERED', + spreadsheetId: resolvedSpreadsheetId, + range: resolvedRange, + valueInputOption, resource: { - range, + range: resolvedRange, majorDimension: 'ROWS', - values: values, + values: resolvedValues, }, }, function (err, response) { @@ -123,14 +125,12 @@ export function appendValues(params, callback = s => 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, + }); } - } + }, ); }); }; @@ -139,56 +139,46 @@ export function appendValues(params, callback = s => s) { /** * Batch update values in a Spreadsheet. * @example - * 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'], + * Update multiple separate 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' } + * ) * @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.valueInputOption] (Optional) Value update options. Defaults to 'USER_ENTERED' - * @param {array} [params.values] A 2d array of values to update. - * @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'. * @returns {Operation} spreadsheet information */ -export function batchUpdateValues(params, callback = s => s) { +export function batchUpdateValues(spreadsheetId, data, options = {}) { return async state => { - const [resolvedParams] = expandReferences(state, params); - - const { - spreadsheetId, - range, - valueInputOption = 'USER_ENTERED', - values, - } = resolvedParams; + const [resolvedSpreadsheetId, resolvedData, resolvedOptions] = + expandReferences(state, spreadsheetId, data, options); + const { valueInputOption = 'USER_ENTERED' } = resolvedOptions; - if (!values || values.length === 0) { - console.log('Warning: empty values array'); + if (!resolvedData || resolvedData.length === 0) { + console.log('Warning: empty data array'); return state; } const resource = { - data: [ - { - range, - values, - }, - ], + data: resolvedData, valueInputOption, }; try { const response = await client.spreadsheets.values.batchUpdate({ - spreadsheetId, + spreadsheetId: resolvedSpreadsheetId, 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; @@ -204,15 +194,14 @@ 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, spreadsheetId, - range + range, ); try { @@ -225,7 +214,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 18a84935a7..de377fd9cc 100644 --- a/packages/googlesheets/test/index.js +++ b/packages/googlesheets/test/index.js @@ -1,70 +1,140 @@ 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('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 }; + beforeEach(() => { + sandbox = sinon.createSandbox(); + + mockAppend = sandbox.stub().callsArgWith(1, null, { + data: { updates: { updatedCells: 4 } }, + }); + + sandbox.stub(google, 'sheets').returns({ + spreadsheets: { + values: { append: mockAppend }, }, - state => { - return { counter: 3 }; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('appends rows to the given range', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; + + const result = await execute( + appendValues('123-456-789', 'Sheet1!A1:B1', [['a', 'b'], ['c', 'd']]) + )(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', () => { + 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('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']] }, ]; - execute(...operations)(state) - .then(finalState => { - expect(finalState).to.eql({ counter: 3 }); - }) - .then(done) - .catch(done); + 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'); }); - it('assigns references, data to the initialState', () => { - const state = { configuration: {}, data: {} }; + it('sends a single range entry to the API', async () => { + const state = { configuration: { access_token: 'mock-token' }, data: {} }; - execute()(state).then(finalState => { - expect(finalState).to.eql({ - references: [], - data: null, - }); - }); + 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'); }); }); +describe('getValues', () => { + let sandbox; + let mockGet; -describe('append', () =>{ - it('should return early if the values array is undefined or nullish', async() => { - const state = { - data: [], - } + beforeEach(() => { + sandbox = sinon.createSandbox(); - const result = await appendValues({ - spreadsheetId: '123-456-789', - range: 'Sheet!A1:E1', - values: state.values, - })(state); - expect(result).to.eql(state); + mockGet = sandbox.stub().resolves({ + data: { values: [['a', 'b'], ['c', 'd']] }, + }); + + sandbox.stub(google, 'sheets').returns({ + spreadsheets: { + values: { get: mockGet }, + }, + }); }); -}); -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); + 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']] }); }); }); 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