2024-05-30 (C) Questetra, Inc. (MIT License) 3 2 This item updates values of cells and gets updated values of other cells on Google Sheets. この工程は、Google スプレッドシートのセルの値を更新し、別のセルの更新後の値を取得します。 https://support.questetra.com/bpmn-icons/service-task-google-sheets-cell-updateandget/ https://support.questetra.com/ja/bpmn-icons/service-task-google-sheets-cell-updateandget/ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADbElEQVRYR8WXS0hUURjH/2dGJFDz lqNimmORhAU5hRVFojgTQSTarl3azN5x1aboQdAqGoVc2AyOFoGCpQuN7E6MRIVW4BBZUqJkbYTw +H7M48Sdx3Ve986d0crFuDjfd8/v+5/vcQ7Bf/4jyezPWfX1IKQKDDoAHIj/P8AwBoCCYAwETnqF 71f63YQAXEc1B19aE8DMAOGUfZhRgFig8rTQRieV85EF4Gw1DQC5r3zj6K0YBUMjNTn6pCAkATir 3gJCmpRFnNDKQo18czyruACcVW8HIZcTfjYpA2anRkdjtEsMQHjkvefuoqawQvTxMV8w54S8Y2DM /4uhmRGUZhfj9gcbBn+8lcZirIWaHOZwgwiAwJmrOkIGzro2lOeUJozzxcwITuYf8ts1v2lB39Sw HMTF8JwQAQLZrp4KT7hkAbj0LMxvLOHquwfonuQlIBiFyrsvVB2bADbDTQA3wr1SARD8F9zLuD7a jq6JQSklblEjL+yHTQCrQWgk2akC7FCnY2d6hug+uzqHSy+vSapAjY5dIkCwwz2LtlaqwNz6Ihbd yxHuK551nHpqSpgLfgWkal4pQLxdlj1rKOqqTVgRAQCbwQmgKlUFUgIAhqmRrw4pMA1CtHIAK541 LGxEyhxtr1apkbsjMC4SK4AxauKPhhRg8aIIPwLX7294NPEch3fvR/d3HpUFOuzJ0IhuHuYF//M9 us/eUQYgjE8jT5ICGJoZhU5TivbxPpwvPo2izLxNAJ8X9omBFAGs+oRHICjw6tdHHNMcRNvnXtRq z2BvZr4I4PZ58PBLfxIAzEWNDp3iJBQAeiYdOJJzAJ1fB2AoOoHCzFwRwOvzon/6dRIAkUkYd/RG 58Do7DgqcsvQ+qkHdSWV0GYVRChwz/VEOUBwMIWqQLhqyTaibU9CxvxDSXErFspwyb0q3VgAqAiB RkkZMsxTE++v120ZRik0ojjDSBjH3rTp8IH0V1oxwzzUnpKYcRxoyZEXkqELrTieVyYru9Si0DW1 j+tjl4NnH1qQvZKltLO8kyi9JEBwOm7/pZSxTmpyNETz/atreUzksgqEFv0XFRB79E1J8dEICQfW kNLDRIQIPM3MYDArBhE2JhCeZpYtPc2iIw0ogmoQogMYB5DygA1zAYSCMeGR6pSLWHEOKJZ5i4Z/ AEwJzTC2ALrNAAAAAElFTkSuQmCC } cellsToUpdate * @param {Array} valuesToUpdate * @param {Array} cellsToGet * @param {Array} valueDefTypes * @return {Array} valueDefs */ const prepareConfigs = (sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, cellsToGet, valueDefTypes) => { const oauth2 = httpClient.createAuthSettingOAuth2( 'Google Sheets', 'https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force', 'https://accounts.google.com/o/oauth2/token', 'https://www.googleapis.com/auth/spreadsheets', 'client_id', 'client_secret', 'access_token' ); configs.putObject('conf_OAuth2', oauth2); // スプレッドシートの ID を設定した文字型データ項目(単一行)を準備 const spreadSheetIdDef = engine.createDataDefinition('スプレッドシートの ID', 1, 'q_SpreadSheetId', 'STRING_TEXTFIELD'); engine.setData(spreadSheetIdDef, sheetId); configs.putObject('conf_SheetId', spreadSheetIdDef); // シートのタイトルを設定した文字型データ項目(単一行)を準備 const sheetTitleDef = engine.createDataDefinition('シートのタイトル', 2, 'q_SheetTitle', 'STRING_TEXTFIELD'); engine.setData(sheetTitleDef, sheetTitle); configs.putObject('conf_SheetTitle', sheetTitleDef); configs.put('conf_ValueInputOption', ''); // テスト時に値が null にならないように空文字を設定 const valueDefs = []; for (let i = 0; i < CELL_NUM; i++) { configs.put(`conf_CellToUpdate${i + 1}`, cellsToUpdate[i]); configs.put(`conf_ValueToUpdate${i + 1}`, valuesToUpdate[i]); configs.put(`conf_CellToGet${i + 1}`, cellsToGet[i]); const valueDefType = valueDefTypes[i]; if (valueDefType === '') { valueDefs.push(null); continue; } else { const valueDef = engine.createDataDefinition(`Value of Cell ${i + 1}`, i + 1, `q_value${i + 1}`, `STRING_${valueDefType}`); engine.setData(valueDef, '事前文字列'); configs.putObject(`conf_ValueDef${i + 1}`, valueDef); valueDefs.push(valueDef); } } return valueDefs; }; /** * 異常系のテスト * @param errorMsg */ const assertError = (errorMsg) => { let failed = false; try { main(); } catch (e) { failed = true; expect(e.message).toEqual(errorMsg); } if (!failed) { fail('No error was thrown.'); } }; const BLANKS = ['', '', '', '']; /** * 更新するセルも取得するセルも設定されていない */ test('No cells to update or get', () => { prepareConfigs('12345abcde', 'シート 1', BLANKS, BLANKS, BLANKS, BLANKS); assertError('No cells to update or get.'); }); /** * 更新後の値だけ設定されていて、更新するセルが設定されていない */ test('Cell to update is empty while its new value is specified', () => { prepareConfigs('12345abcde', 'シート 1', BLANKS, ['新しい値', '', '', ''], BLANKS, BLANKS); assertError('Cell 1 to update is empty while its new value is specified.'); }); /** * 更新するセルの書式が不正 */ test('Cell to update is invalid', () => { prepareConfigs('12345abcde', 'シート 1', ['', '1A', '', ''], BLANKS, BLANKS, BLANKS); assertError('Cell 2 to update is invalid.'); }); /** * 指定の長さの文字列を作成 * @param length * @return string */ const createString = (length) => { const sourceStr = 'あいうえおかきくけこ'; const string = sourceStr.repeat(Math.floor(length / sourceStr.length)) + sourceStr.slice(0, length % sourceStr.length); return string; } /** * 更新後の値が最大文字数を超えている */ test('New value exceeds maximum length', () => { const newValue = createString(VALUE_MAX_LENGTH + 1); prepareConfigs('12345abcde', 'シート 1', ['', 'A1', '', ''], ['', newValue, '', ''], BLANKS, BLANKS); assertError(`New value for Cell 2 exceeds ${VALUE_MAX_LENGTH} characters.`); }); /** * 値を保存するデータ項目だけ設定されていて、取得するセルが設定されていない */ test('Cell to get is empty while data item to save its value is specified', () => { prepareConfigs('12345abcde', 'シート 1', BLANKS, BLANKS, BLANKS, ['', '', 'TEXTFIELD', '']); assertError('Cell 3 to get and data item to save its value must be specified at the same time.'); }); /** * 取得するセルだけ設定されていて、値を保存するデータ項目が設定されていない */ test('Cell to get is specified while data item to save the value is blank', () => { prepareConfigs('12345abcde', 'シート 1', BLANKS, BLANKS, ['', '', '', 'A1'], BLANKS); assertError('Cell 4 to get and data item to save its value must be specified at the same time.'); }); /** * 取得するセルの書式が不正 */ test('Cell to get is invalid', () => { prepareConfigs('12345abcde', 'シート 1', BLANKS, BLANKS, ['A:A', '', '', ''], ['TEXTFIELD', '', '', '']); assertError('Cell 1 to get is invalid.'); }); /** * セル更新の POST リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.headers * @param request.contentType * @param request.body * @param sheetId * @param sheetTitle * @param cellsToUpdate * @param valuesToUpdate */ const assertPostRequest = ({url, method, headers, contentType, body}, sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, valueInputOption = 'RAW') => { expect(url).toEqual(`${GOOGLE_API_URI}spreadsheets/${encodeURIComponent( sheetId )}/values:batchUpdate`); expect(method).toEqual('POST'); expect(headers.Authorization).toEqual('Bearer access_token'); expect(contentType).toEqual('application/json'); const bodyObj = JSON.parse(body); expect(bodyObj.valueInputOption).toEqual(valueInputOption); expect(bodyObj.data.length).toEqual(cellsToUpdate.length); cellsToUpdate.forEach((cell, i) => { expect(bodyObj.data[i].range).toEqual(`${sheetTitle}!${cell}`); expect(bodyObj.data[i].values[0][0]).toEqual(valuesToUpdate[i]); }); }; /** * セル更新の POST リクエストでエラー */ test('Fail in POST request', () => { const sheetId = '12345abcde'; const sheetTitle = 'シート 1'; const cellsToUpdate = ['A1', 'B2', 'C3', 'D4']; const valuesToUpdate = ['新しい値1', '新しい値2', '新しい値3', '新しい値4']; prepareConfigs(sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, BLANKS, BLANKS); httpClient.setRequestHandler((request) => { assertPostRequest(request, sheetId, sheetTitle, cellsToUpdate, valuesToUpdate); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); assertError('Failed to update cells. status: 400'); }); /** * セル取得の GET リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.headers * @param request.body * @param sheetId * @param sheetTitle * @param cellsToGet */ const assertGetRequest = ({url, method, headers}, sheetId, sheetTitle, cellsToGet) => { let expectedUrl = `${GOOGLE_API_URI}spreadsheets/${encodeURIComponent(sheetId)}/values:batchGet` + '?valueRenderOption=UNFORMATTED_VALUE' + '&dateTimeRenderOption=FORMATTED_STRING'; cellsToGet.forEach((cell) => { expectedUrl += `&ranges=${encodeURIComponent(sheetTitle)}!${cell}`; }); expectedUrl = expectedUrl .replace(/%20/g, '+') // HttpRequestWrapper#queryParam() はスペースを + に置き換える .replace(/\!/g, '%21') // encodeURIComponent() でエンコードされない文字をエンコード .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29'); expect(url).toEqual(expectedUrl); expect(method).toEqual('GET'); expect(headers.Authorization).toEqual('Bearer access_token'); }; /** * セル取得の GET リクエストでエラー */ test('Fail in GET request', () => { const sheetId = '12345abcde'; const sheetTitle = 'シート 1'; const cellsToGet = ['A1', 'B2', 'C3', 'D4']; const valueDefTypes = ['TEXTFIELD', 'TEXTFIELD', 'TEXTFIELD', 'TEXTFIELD']; prepareConfigs(sheetId, sheetTitle, BLANKS, BLANKS, cellsToGet, valueDefTypes); httpClient.setRequestHandler((request) => { assertGetRequest(request, sheetId, sheetTitle, cellsToGet); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); assertError('Failed to get cells. status: 400'); }); /** * セル取得のレスポンスを準備 * @param sheetTitle * @param cells * @param cellValues * @returns {String} */ const prepareGetResponse = (sheetTitle, cells, cellValues) => { const valueRanges = []; cells.forEach((cell, i) => { const valueRange = { range: `${sheetTitle}!${cell}`, majorDimension: 'ROWS' }; const cellValue = cellValues[i]; if (cellValue !== null) { valueRange.values = [[cellValue]]; } valueRanges.push(valueRange); }); return JSON.stringify({valueRanges}); }; /** * 単一行データ項目に複数行の文字列を保存しようとしてエラーになる場合 */ test('Validation Error - Unable to save multi-line string to STRING_TEXTFIELD', () => { const sheetId = '12345abcde'; const sheetTitle = 'シート 1'; const cellsToGet = ['', '', '', 'A1']; const valueDefTypes = ['', '', '', 'TEXTFIELD']; prepareConfigs(sheetId, sheetTitle, BLANKS, BLANKS, cellsToGet, valueDefTypes); const trimmedCellsToGet = ['A1']; const cellValues = ['複数行の\n文字列']; httpClient.setRequestHandler((request) => { assertGetRequest(request, sheetId, sheetTitle, trimmedCellsToGet); return httpClient .createHttpResponse(200, 'application/json', prepareGetResponse(sheetTitle, trimmedCellsToGet, cellValues)); }); try { main(); fail(); } catch (e) { // エラーになるのが正しい } }); /** * 成功 - すべてのセルを指定。valueInputOption 指定なし */ test('Succeed - all cells specified, valueInputOption not specified', () => { const sheetId = '12345abcde'; const sheetTitle = 'シート 1'; const cellsToUpdate = ['A1', 'B1', 'C1', 'D1']; const valuesToUpdate = ['新しい値1', '新しい値2', '新しい値3', '新しい値4']; const cellsToGet = ['AA100', 'BB200', 'CC300', 'XD400']; const valueDefTypes = ['TEXTFIELD', 'TEXTAREA', 'TEXTFIELD', 'TEXTAREA']; const valueDefs = prepareConfigs(sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, cellsToGet, valueDefTypes); let reqCount = 0; const cellValues = ['文字列', '改行を含む\n文字列', 123.56, true]; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertPostRequest(request, sheetId, sheetTitle, cellsToUpdate, valuesToUpdate); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{}'); } assertGetRequest(request, sheetId, sheetTitle, cellsToGet); return httpClient .createHttpResponse(200, 'application/json', prepareGetResponse(sheetTitle, cellsToGet, cellValues)); }); expect(main()).toEqual(undefined); expect(engine.findData(valueDefs[0])).toEqual('文字列'); expect(engine.findData(valueDefs[1])).toEqual('改行を含む\n文字列'); expect(engine.findData(valueDefs[2])).toEqual('123.56'); expect(engine.findData(valueDefs[3])).toEqual('true'); }); /** * 成功 - 更新のみ。valueInputOption に RAW を指定 */ test('Succeed - update some cells, select RAW', () => { const sheetId = '6789hijfk'; const sheetTitle = 'シート (2)'; const cellsToUpdate = ['XA999', '', 'ZB1000', '']; const valuesToUpdate = ['', '', '新しい値', '']; prepareConfigs(sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, BLANKS, BLANKS); configs.put('conf_ValueInputOption', 'RAW'); const trimmedCellsToUpdate = ['XA999', 'ZB1000']; const trimmedValuesToUpdate = ['', '新しい値']; httpClient.setRequestHandler((request) => { assertPostRequest(request, sheetId, sheetTitle, trimmedCellsToUpdate, trimmedValuesToUpdate, 'RAW'); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); expect(main()).toEqual(undefined); }); /** * 成功 - 更新のみ。valueInputOption に USER_ENTERED を指定 */ test('Succeed - update some cells, select USER_ENTERED', () => { const sheetId = '6789hijfk'; const sheetTitle = 'シート (2)'; const cellsToUpdate = ['A1', '', '', '']; const valuesToUpdate = ['123.45', '', '', '']; prepareConfigs(sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, BLANKS, BLANKS); configs.put('conf_ValueInputOption', 'USER_ENTERED'); const trimmedCellsToUpdate = ['A1']; const trimmedValuesToUpdate = ['123.45']; httpClient.setRequestHandler((request) => { assertPostRequest(request, sheetId, sheetTitle, trimmedCellsToUpdate, trimmedValuesToUpdate, 'USER_ENTERED'); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); expect(main()).toEqual(undefined); }); /** * 成功 - 取得のみ */ test('Succeed - get some cells', () => { const sheetId = '12345abcde'; const sheetTitle = 'シート_1'; const cellsToGet = ['', 'XXA1', 'ZZB2', '']; const valueDefTypes = ['', 'TEXTFIELD', 'TEXTFIELD', '']; const valueDefs = prepareConfigs(sheetId, sheetTitle, BLANKS, BLANKS, cellsToGet, valueDefTypes); const trimmedCellsToGet = ['XXA1', 'ZZB2']; const cellValues = [null, '文字列']; httpClient.setRequestHandler((request) => { assertGetRequest(request, sheetId, sheetTitle, trimmedCellsToGet); return httpClient .createHttpResponse(200, 'application/json', prepareGetResponse(sheetTitle, trimmedCellsToGet, cellValues)); }); expect(main()).toEqual(undefined); expect(valueDefs[0]).toEqual(null); expect(engine.findData(valueDefs[1])).toEqual(''); expect(engine.findData(valueDefs[2])).toEqual('文字列'); expect(valueDefs[3]).toEqual(null); }); /** * 設定の準備 * スプレッドシートの ID とシートのタイトルは直接指定 * @param {String} sheetId * @param {String} sheetTitle * @param {Array} cellsToUpdate * @param {Array} valuesToUpdate * @param {Array} cellsToGet * @param {Array} valueDefTypes * @return {Array} valueDefs */ const prepareConfigsWithFixedValue = (sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, cellsToGet, valueDefTypes) => { const oauth2 = httpClient.createAuthSettingOAuth2( 'Google Sheets', 'https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force', 'https://accounts.google.com/o/oauth2/token', 'https://www.googleapis.com/auth/spreadsheets', 'client_id', 'client_secret', 'access_token' ); configs.putObject('conf_OAuth2', oauth2); configs.put('conf_SheetId', sheetId); configs.put('conf_SheetTitle', sheetTitle); configs.put('conf_ValueInputOption', ''); // テスト時に値が null にならないように空文字を設定 const valueDefs = []; for (let i = 0; i < CELL_NUM; i++) { configs.put(`conf_CellToUpdate${i + 1}`, cellsToUpdate[i]); configs.put(`conf_ValueToUpdate${i + 1}`, valuesToUpdate[i]); configs.put(`conf_CellToGet${i + 1}`, cellsToGet[i]); const valueDefType = valueDefTypes[i]; if (valueDefType === '') { valueDefs.push(null); continue; } else { const valueDef = engine.createDataDefinition(`Value of Cell ${i + 1}`, i + 1, `q_value${i + 1}`, `STRING_${valueDefType}`); engine.setData(valueDef, '事前文字列'); configs.putObject(`conf_ValueDef${i + 1}`, valueDef); valueDefs.push(valueDef); } } return valueDefs; }; /** * 成功 - すべてのセルを指定。valueInputOption 指定なし * スプレッドシートの ID とシートのタイトルは固定値で指定 */ test('Succeed - Set the Spreadsheet ID and Title as a fixed value, all cells specified, valueInputOption not specified', () => { const sheetId = '12345abcde'; const sheetTitle = 'シート 1'; const cellsToUpdate = ['A1', 'B1', 'C1', 'D1']; const valuesToUpdate = ['新しい値1', '新しい値2', '新しい値3', '新しい値4']; const cellsToGet = ['AA100', 'BB200', 'CC300', 'XD400']; const valueDefTypes = ['TEXTFIELD', 'TEXTAREA', 'TEXTFIELD', 'TEXTAREA']; const valueDefs = prepareConfigsWithFixedValue(sheetId, sheetTitle, cellsToUpdate, valuesToUpdate, cellsToGet, valueDefTypes); let reqCount = 0; const cellValues = ['文字列', '改行を含む\n文字列', 123.56, true]; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertPostRequest(request, sheetId, sheetTitle, cellsToUpdate, valuesToUpdate); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{}'); } assertGetRequest(request, sheetId, sheetTitle, cellsToGet); return httpClient .createHttpResponse(200, 'application/json', prepareGetResponse(sheetTitle, cellsToGet, cellValues)); }); expect(main()).toEqual(undefined); expect(engine.findData(valueDefs[0])).toEqual('文字列'); expect(engine.findData(valueDefs[1])).toEqual('改行を含む\n文字列'); expect(engine.findData(valueDefs[2])).toEqual('123.56'); expect(engine.findData(valueDefs[3])).toEqual('true'); }); ]]>