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');
});
]]>