kintone: Download Choice Data
Download Choice Data in specified two fields from a Kintone App.
2020-10-15 (C) Questetra, Inc. (MIT License)
Configs
  • C1: Authorization Setting in which API Token is set *
  • C2: Domain (such as xxxxx.kintone.com or xxxxx.cybozu.com) *
  • C3: Guest Space ID (required if the App is in a Guest Space)
  • C4: App ID *
  • C5: Field Code of Choice IDs *
  • C6: Field Code of Choice Labels *
  • C7: Search Query #{EL}
  • C8: String type data item that will save Choise IDs *
  • C9: String type data item that will save Choice Labels *
Script
main();
function main(){
//// == 工程コンフィグの参照 / Config Retrieving ==
const auth = configs.get("conf_auth");
const domain = configs.get("conf_domain");
const guestSpaceId = configs.get("conf_guestSpaceId");
const appId = configs.get("conf_appId");
const idField = configs.get("conf_idField");
const labelField = configs.get("conf_labelField");
const query = configs.get("conf_query");
const idsDef = configs.getObject("conf_ids");
const labelsDef = configs.getObject("conf_labels");

//// == ワークフローデータの参照 / Data Retrieving ==
const apiToken = httpClient.getOAuth2Token( auth );

//// == 演算 / Calculating ==
const apiUri = determineApiUri( domain, guestSpaceId );
checkAppId( appId );

checkFields( apiUri, apiToken, appId, idField, labelField );

const initialParams = prepareInitialParams( appId, idField, labelField, query );
const records = []; // レコードオブジェクトを格納する配列
const requestNum = 1; // 現時点での HTTP リクエスト回数
getRecords( apiUri, apiToken, initialParams, records, requestNum );

const idList = records.map( record => record[idField].value );
const labelList = records.map( record => record[labelField].value );
checkValues( idList, labelList );

//// == ワークフローデータへの代入 / Data Updating ==
setData( idsDef, idList );
setData( labelsDef, labelList );
}

/**
* kintone REST API のレコード取得の URI を決定する
* ドメインが空、または kintone のドメインとして不正な文字列であればエラーとする
* @param {String} domain ドメイン
* @param {String} guestSpaceId ゲストスペース ID
* @return {String} apiUri API の URI
*/
function determineApiUri( domain, guestSpaceId ) {
checkDomain( domain );
let apiUri;
if ( guestSpaceId === "" || guestSpaceId === null ) {
apiUri = `https://${domain}/k/v1/`;
} else {
if ( !isValidId(guestSpaceId) ) {
throw "Invalid Guest Space ID.";
}
apiUri = `https://${domain}/k/guest/${guestSpaceId}/v1/`;
}
return apiUri;
}

/**
* ドメインが空または不正な文字列であればエラーとする
* @param {String} domain ドメイン
*/
function checkDomain( domain ) {
if ( domain === "" || domain === null ) {
throw "Domain is empty.";
}
const reg = new RegExp( '^[0-9a-zA-Z-]{3,32}.(?:kintone.com|cybozu.com)$' );
if ( !reg.test(domain) ) {
throw "Invalid Kintone domain.";
}
}

/**
* アプリ ID が空または不正な文字列であればエラーとする
* @param {String} appId アプリ ID
*/
function checkAppId( appId ) {
if ( appId === "" || appId === null ) {
throw "App ID is empty.";
}
if ( !isValidId(appId) ) {
throw "Invalid App ID.";
}
}

/**
* ID が有効か(自然数か)を判定する
* @param {String} idString ID の文字列
* @return {Boolean} 有効な ID かどうか
*/
function isValidId( idString ) {
const idReg = new RegExp( '^[1-9][0-9]*$' );
return idReg.test( idString );
}

/**
* kintone REST API にフィールド一覧取得の GET リクエストを送信し、
* 指定したフィールドの存在とフィールド型をチェックする
* サポートするフィールド型: レコードID, レコード番号, 文字列(1行), リンク, 数値, 日付, 時刻, 日時, 計算
* @param {String} apiUri API の URI
* @param {String} apiToken API トークン
* @param {String} appId アプリ ID
* @param {String} idField 選択肢 ID のフィールドコード
* @param {String} labelField 選択肢ラベルのフィールドコード
*/
function checkFields( apiUri, apiToken, appId, idField, labelField ) {
const getFieldsUri = `${apiUri}app/form/fields.json?app=${appId}`;
engine.log(`API URI: ${getFieldsUri}`);
const response = httpClient.begin()
.header( "X-Cybozu-API-Token", apiToken )
.get( getFieldsUri );
//when error thrown
const responseJson = response.getResponseAsString();
const status = response.getStatusCode();
const accessLog = `---GET request--- ${status}\n${responseJson}\n`;
engine.log(accessLog);
if (status >= 300) {
throw `Failed to get form fields. status: ${status}`;
}
const json = JSON.parse(responseJson);

// フィールドコードの存在とフィールド型をチェック(フィールドコードが'$id'であればチェック不要)
const supportedFieldTypes = new Set( [ "RECORD_NUMBER", "SINGLE_LINE_TEXT", "LINK", "NUMBER", "DATE", "TIME", "DATETIME", "CALC" ] );
if ( idField !== '$id' ) {
if ( json.properties[idField] === undefined ) {
throw `${idField} does not exist in the app.`;
}
if ( !supportedFieldTypes.has( json.properties[idField].type ) ) { // idField のフィールド型がサポート外であればエラー
throw `Unable to use ${idField} as Choice ID. Field Type ${json.properties[idField].type} is not supported.`;
}
}
if ( labelField !== '$id' ) {
if ( json.properties[labelField] === undefined ) {
throw `${labelField} does not exist in the app.`;
}
if ( !supportedFieldTypes.has( json.properties[labelField].type ) ) { // labelField のフィールド型がサポート外であればエラー
throw `Unable to use ${labelField} as Choice Label. Field Type ${json.properties[labelField].type} is not supported.`;
}
}
}

/**
* 初回の GET リクエストのパラメータに使用する情報を準備する
* @param {String} appId アプリ ID
* @param {String} idField 選択肢 ID のフィールドコード
* @param {String} labelField 選択肢ラベルのフィールドコード
* @param {String} query 検索クエリ
* @return {Object} initialParams リクエストのパラメータに使用する情報を格納した JSON オブジェクト
* プロパティ: {String} app アプリ ID
* {Set<String>} fields フィールドコードの集合
* {String} query 検索クエリ
* {Number} lastRecordId 検索済みの最後のレコード番号
*/
function prepareInitialParams( appId, idField, labelField, query ) {
const fields = new Set([ idField, labelField ]);
fields.add( '$id' ); // $id で並べ替えを行うため、取得フィールドに $id を追加する
const initialParams = {
app: appId,
fields: fields,
query: query,
lastRecordId: 0
};
return initialParams;
}

/**
* kintone REST API にレコード取得の GET リクエストを送信する
* 未取得のレコードがなくなるまで再帰的に実行される
* @param {String} apiUri API の URI
* @param {String} apiToken API トークン
* @param {Object} params GET リクエストのパラメータに使用する情報が格納されたオブジェクト
* プロパティ: {String} app アプリ ID
* {Set<String>} fields フィールドコードの集合
* {String} query 検索クエリ
* {Number} lastRecordId 検索済みの最後のレコード番号
* @param {Array<Object>} records レコードオブジェクトを格納する配列
* @param {Number} requestNum HTTP リクエスト回数
*/
function getRecords( apiUri, apiToken, { app, fields, query, lastRecordId }, records, requestNum ) {
// リクエスト回数の上限チェック
if ( requestNum + 1 > httpClient.getRequestingLimit() ) {
throw "HTTP requests exceed the limit.";
}

const LIMIT = 500; // 1回の GET リクエストで取得できるレコードの上限件数
const getRecordsUri = `${apiUri}records.json`;
let queryString;
if ( query === "" || query === null ) {
queryString = `query=$id > ${lastRecordId} order by $id asc limit ${LIMIT}`;
} else {
queryString = `query=( ${query} ) and $id > ${lastRecordId} order by $id asc limit ${LIMIT}`;
}
const fieldsString = Array.from( fields, (fieldCode, i) => `fields[${i}]=${fieldCode}` ).join('&');
const paramsString = `app=${app}&${queryString}&${fieldsString}`;
engine.log(`API URI: ${getRecordsUri}`);
engine.log(`Params: ${paramsString}`);
const uriWithParams = encodeURI( `${getRecordsUri}?${paramsString}` );
const response = httpClient.begin()
.header( "X-Cybozu-API-Token", apiToken )
.get( uriWithParams );
//when error thrown
const responseJson = response.getResponseAsString();
const status = response.getStatusCode();
const accessLog = `---GET request--- ${status}`;
engine.log(accessLog);
if (status >= 300) {
engine.log(responseJson);
throw `Failed to get records. status: ${status}`;
}
const json = JSON.parse(responseJson);
Array.prototype.push.apply(records, json.records); // レコードオブジェクトを配列に追加
requestNum++; // リクエスト回数を加算

// レコード件数のチェック
if ( records.length === 0 ) { // 該当するレコードが一件もなければエラー
throw "No Choice Data found.";
}
if ( records.length > 150000 ) { // 15万件を超える場合はエラー
throw "Number of Choice Data is over 150,000.";
}

// 再帰呼び出し
if ( json.records.length === LIMIT ) {
// 取得レコードの件数が LIMIT と同じ場合は、未取得のレコードが残っている場合があるので、
// lastRecordId を更新し、getRecords を再帰呼び出しする
lastRecordId = json.records[json.records.length - 1].$id.value;
getRecords( apiUri, apiToken, { app, fields, query, lastRecordId }, records, requestNum );
}
}

/**
* 選択肢 ID と選択肢ラベルの配列の値をチェックし、以下の場合はエラーとする
* 1. 空文字列や null が含まれる場合
* 2. idList に重複がある場合
* 3. 1,000文字を超えるものがある場合
* @param {Array<String>} idList 選択肢 ID の配列
* @param {Array<String>} labelList 選択肢ラベルの配列
*/
function checkValues( idList, labelList ) {
// 空文字列や null が含まれる場合はエラー
if ( idList.indexOf("") !== -1 || idList.indexOf(null) !== -1 ) {
throw "Empty data is in Choice IDs.";
}
if ( labelList.indexOf("") !== -1 || labelList.indexOf(null) !== -1 ) {
throw "Empty data is in Choice Labels.";
}

// idList に重複があればエラー
const idSet = new Set( idList ); // idList の重複確認用の Set
if ( idSet.size !== idList.length ) {
throw "Same values are in Choice IDs.";
}

// 1,000文字を超えるものがあればエラー
if ( idList.length !== labelList.length ) { // 次の for 文に備えて配列の長さ一致をチェック
throw "Array length does not match.";
}
for ( let i in idList ) {
if ( idList[i].length > 1000 ) {
throw "Unable to use string over 1,000 characters as Choice ID.";
}
if ( labelList[i].length > 1000 ) {
throw "Unable to use string over 1,000 characters as Choice Label.";
}
}
}

/**
* 文字列データの配列を改行で繋ぎ、データ項目に出力する
* @param {ProcessDataDefinitionView} dataDef 保存先データ項目の ProcessDataDefinitionView
* @param {Array<String>} dataStringList 出力する文字列データの配列
*/
function setData( dataDef, dataStringList ) {
if ( dataDef !== null ) {
engine.setData( dataDef, dataStringList.join('\n') );
}
}

Download

Capture

Notes

  1. To get API Token, open the Settings of the App and click “API Token” in the App Settings tab.

    Click “Generate”, select Permissions (“View records” permission is required), and click “Save”.

    Do not forget to click “Update App” to apply the update.
  2. Guest Space ID (only when the App is in a guest space) and App ID can be confirmed in the API Token settings.
  3. Supported field types are: Record Number, Record ID, Text, Number, Calculated, Link, Date, Time, Date and time, Lookup.
  4. See the Kintone Reference for the operators and functions that can be used in C7: Search Query. Query options (order by, limit, offset) are not supported.
%d bloggers like this: