kintone: レコード検索

kintone: Search Records

この工程は、kintone アプリから検索クエリに合致するレコードを取得します。

Auto Step icon
Basic Configs
工程名
メモ
Configs for this Auto Step
conf_auth
C1: API トークンを設定した認証設定 *
conf_basic
C2: Basic 認証設定(kintone で設定されている場合のみ)
conf_domain
C3: ドメイン(xxxxx.kintone.com または xxxxx.cybozu.com) *
conf_guestSpaceId
C4: ゲストスペース ID(ゲストスペース内のアプリの場合のみ)
conf_appId
C5: アプリ ID *
conf_query
C6: 検索クエリ#{EL}
conf_recordIds
C7: レコード ID の一覧を保存するデータ項目
conf_idField
C8F: フィールドコード_1
conf_ids
C8V: 値_1 の一覧を保存するデータ項目
conf_labelField
C9F: フィールドコード_2
conf_labels
C9V: 値_2 の一覧を保存するデータ項目

Notes

  • kintone アプリのAPI トークンを取得するには、アプリの設定画面の「設定(App Settings)」のタブを開き、「API トークン(API Token)」へと進みます。

    「生成する(Generate)」をクリックし、権限(Permissions)を選択(「レコード参照(View records)」権限が必要です)したあと、「保存(Save)」をクリックします。

    「アプリを更新(Update App)」をクリックして変更を適用するのも忘れないようにしてください。
  • kintone アプリのゲストスペース ID(アプリがゲストスペースにある場合)とアプリ ID は、API トークンの設定画面で確認することができます。
  • このモデリング要素がサポートするフィールド型は、レコード番号レコード IDリビジョンステータス作成日時更新日時文字列(1行)数値計算リンク日付時刻日時ルックアップラジオボタンドロップダウンです。
  • 検索クエリで使用可能な演算子と関数については、kintone のリファレンスを参照してください。オプション(order by, limit, offset)は使用できません。

Capture

See Also

Script (click to open)
  • 次のスクリプトが記述されている XML ファイルをダウンロードできます
    • kintone-record-search.xml (C) Questetra, Inc. (MIT License)
    • Professional のワークフロー基盤では、ファイル内容を改変しオリジナルのアドオン自動工程として活用できます


main();
function main(){
  //// == 工程コンフィグの参照 / Config Retrieving ==
  const auth = configs.getObject("conf_auth");
  const basic = configs.getObject("conf_basic");
  const domain = configs.get("conf_domain");
  const guestSpaceId = configs.get("conf_guestSpaceId");
  const appId = configs.get("conf_appId");
  const query = configs.get("conf_query");
  const fieldCodeList = [
    "$id",
    configs.get("conf_idField"),
    configs.get("conf_labelField")
  ];
  const dataDefList = [
    configs.getObject("conf_recordIds"),
    configs.getObject("conf_ids"),
    configs.getObject("conf_labels")
  ];

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

  //// == 演算 / Calculating ==
  const singleLineFlag = checkFieldCodesAndDataDefs( fieldCodeList, dataDefList );

  const apiUri = determineApiUri( domain, guestSpaceId );
  checkAppId( appId );

  const fields = new Set( fieldCodeList );
  fields.delete(null);
  fields.delete("");

  // フィールドの存在とフィールド型をチェック(requestNum は現時点での HTTP リクエスト回数)
  const requestNum = checkFields( apiUri, apiToken, basic, appId, fields );

  const initialParams = prepareInitialParams( appId, fields, query );
  const records = []; // レコードオブジェクトを格納する配列
  getRecords( apiUri, apiToken, basic, initialParams, records, requestNum, singleLineFlag );

  //// == ワークフローデータへの代入 / Data Updating ==
  saveData( records, fieldCodeList, dataDefList );
}

/**
  * フィールドコードと保存先データ項目をチェックし、以下の場合はエラーとする
  * 1. 保存先データ項目が一つも設定されていない
  * 2. フィールドコードが空なのに、保存先データ項目が設定されている
  * 3. フィールドコードが設定されているのに、保存先データ項目が設定されていない
  * @param {Array<String>} fieldCodeList  フィールドコードの配列(1番目は $id)
  * @param {Array<ProcessDataDefinitionView>} dataDefList  保存先データ項目の配列
  * @return {boolean} singleLineFlag  保存先データ項目のいずれかが単一行であれば true
  */
function checkFieldCodesAndDataDefs( fieldCodeList, dataDefList ) {
  // 保存先データ項目が一つも設定されていなければエラー
  if ( dataDefList.filter(dataDef => dataDef !== null).length === 0 ) {
    throw "No data item to save the result is set.";
  }

  // フィールドコードと、値を保存するデータ項目をチェック
  for (let i = 1; i < fieldCodeList.length; i++) {
    if ( fieldCodeList[i] === "" || fieldCodeList[i] === null ) { // フィールドコードが空
      if ( dataDefList[i] !== null ) { // 保存先データ項目が設定されている
        throw `Field Code ${i} is empty but the data item to save the values is set.`;
      }
    } else { // フィールドコードが空でない
      if ( dataDefList[i] === null ) { // 保存先データ項目が設定されていない
        throw `Data item to save the values of Field Code ${i} is not set.`;
      }
    }
  }

  return dataDefList.some(dataDef => dataDef !== null && dataDef.matchDataType("STRING_TEXTFIELD"));
}

/**
  * 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 ) { // required="true" なので空になることはないが、チェック
    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 ) { // required="true" なので空になることはないが、チェック
    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 と $revision 以外のフィールドが指定されていない場合はリクエストを送信しない
  *  サポートするフィールド型:
  *    レコードID, リビジョン, レコード番号, 文字列(1行), リンク, 数値, 日付, 時刻, 日時, 計算,
  *    ドロップダウン, ラジオボタン, ステータス, 作成日時, 更新日時
  * @param {String} apiUri  API の URI
  * @param {String} apiToken  API トークン
  * @param {AuthSettingWrapper} basic  Basic 認証設定
  * @param {String} appId  アプリ ID
  * @param {Set<String>} fields  フィールドコードの集合
  * @return {Number} requestNum  HTTP リクエスト回数(リクエストを送信したら 1, 送信しなければ 0)
  */
function checkFields( apiUri, apiToken, basic, appId, fields ) {
  const fieldsToCheck = new Set(fields);
  fieldsToCheck.delete("$id");
  fieldsToCheck.delete("$revision");

  if ( fieldsToCheck.size === 0 ) { // チェックすべきフィールドがない
    return 0;
  }

  const getFieldsUri = `${apiUri}app/form/fields.json`;
  let request = httpClient.begin()
    .queryParam("app", appId)
    .header( "X-Cybozu-API-Token", apiToken );
  if (basic !== null) {
    request = request.authSetting(basic);
  }
  const response = request.get( getFieldsUri );
  //when error thrown
  const responseStr = response.getResponseAsString();
  const status = response.getStatusCode();
  if (status >= 300) {
    engine.log(`---GET request--- ${status}\n${responseStr}\n`);
    throw `Failed to get form fields. status: ${status}`;
  }
  const json = JSON.parse(responseStr);

  const supportedFieldTypes = new Set([
    "RECORD_NUMBER", "SINGLE_LINE_TEXT", "LINK", "NUMBER", "DATE", "TIME", "DATETIME", "CALC",
    "DROP_DOWN", "RADIO_BUTTON", "STATUS", "CREATED_TIME", "UPDATED_TIME"
  ]);
  fieldsToCheck.forEach(fieldCode => {
    if ( json.properties[fieldCode] === undefined ) {
      throw `Field Code: ${fieldCode} does not exist in the app.`;
    }
    if ( !supportedFieldTypes.has( json.properties[fieldCode].type ) ) { // フィールド型がサポート外であればエラー
      throw `Unable to save the values of ${fieldCode}. Field Type ${json.properties[fieldCode].type} is not supported.`;
    }
  });
  return 1;
}

/**
  * 初回の GET リクエストのパラメータに使用する情報を準備する
  * @param {String} appId  アプリ ID
  * @param {Set<String>} fields  フィールドコードの集合
  * @param {String} query  検索クエリ
  * @return {Object} initialParams  リクエストのパラメータに使用する情報を格納した JSON オブジェクト
  *   プロパティ:  {String} app  アプリ ID
  *              {Set<String>} fields  フィールドコードの集合
  *              {String} query  検索クエリ
  *              {Number} lastRecordId  検索済みの最後のレコード番号
  */
function prepareInitialParams( appId, fields, query ) {
  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 {AuthSettingWrapper} basic  Basic 認証設定
  * @param {Object} params  GET リクエストのパラメータに使用する情報が格納されたオブジェクト
  *   プロパティ:  {String} app  アプリ ID
  *              {Set<String>} fields  フィールドコードの集合
  *              {String} query  検索クエリ
  *              {Number} lastRecordId  検索済みの最後のレコード番号
  * @param {Array<Object>} records  レコードオブジェクトを格納する配列
  * @param {Number} requestNum  HTTP リクエスト回数
  * @param {boolean} singleLineFlag  保存先データ項目のいずれかが単一行であれば true
  */
function getRecords( apiUri, apiToken, basic, { app, fields, query, lastRecordId }, records, requestNum, singleLineFlag ) {
  // リクエスト回数の上限チェック
  if ( requestNum + 1 > httpClient.getRequestingLimit() ) {
    throw "HTTP requests exceed the limit.";
  }

  const LIMIT = 500; // 1回の GET リクエストで取得できるレコードの上限件数
  const getRecordsUri = `${apiUri}records.json`;
  let request = httpClient.begin()
    .queryParam("app", app)
    .header( "X-Cybozu-API-Token", apiToken );
  if (basic !== null) {
    request = request.authSetting(basic);
  }
  // query パラメータの設定
  if ( query === "" || query === null ) {
    request = request.queryParam("query", `$id > ${lastRecordId} order by $id asc limit ${LIMIT}`);
  } else {
    request = request.queryParam("query", `( ${query} ) and $id > ${lastRecordId} order by $id asc limit ${LIMIT}`);
  }
  // fields パラメータの設定
  Array.from(fields.values()).forEach((fieldCode, i) => {
    request = request.queryParam(`fields[${i}]`, fieldCode);
  });
  const response = request.get( getRecordsUri );
  //when error thrown
  const responseStr = response.getResponseAsString();
  const status = response.getStatusCode();
  if (status >= 300) {
    engine.log(`---GET request--- ${status}\n${responseStr}\n`);
    throw `Failed to get records. status: ${status}`;
  }
  const json = JSON.parse(responseStr);
  Array.prototype.push.apply(records, json.records); // レコードオブジェクトを配列に追加
  requestNum++; // リクエスト回数を加算

  // レコード件数のチェック
  if ( records.length === 0 ) { // 該当するレコードが一件もなければエラー
    throw "No records found.";
  }
  if ( singleLineFlag && records.length > 1 ) { // 保存先データ項目が単一行なのにレコードが複数件あればエラー
    throw "Multiple records were found while the data item to save the result is Single-Line.";
  }

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

/**
  * レコードからデータを読み出し、データ項目に出力する
  * @param {Array<Object>} records  レコードオブジェクトが格納された配列
  * @param {Array<String>} fieldCodeList  フィールドコードの配列
  * @param {Array<ProcessDataDefinitionView>} dataDefList  保存先データ項目の配列
  */
function saveData( records, fieldCodeList, dataDefList ) {
  dataDefList.forEach((dataDef, i) => {
    if ( dataDef === null ) {
      return;
    }
    const dataList = records.map( record => record[fieldCodeList[i]].value );
    engine.setData( dataDef, dataList.join("\n") );
  });
}

%d人のブロガーが「いいね」をつけました。