kintone: Search Records

kintone: レコード検索

This item searches records in a Kintone App using a query.

Auto Step icon
Basic Configs
Step Name
Note
Configs for this Auto Step
conf_auth
C1: Authorization Setting in which API Token is set *
conf_basic
C2: Basic Auth Setting (required if enabled on Kintone)
conf_domain
C3: Domain (such as xxxxx.kintone.com or xxxxx.cybozu.com) *
conf_guestSpaceId
C4: Guest Space ID (required if the App is in a Guest Space)
conf_appId
C5: App ID *
conf_query
C6: Search Query#{EL}
conf_recordIds
C7: Data item to save Record IDs
conf_idField
C8F: Field Code 1
conf_ids
C8V: Data item to save Values of Field Code 1
conf_labelField
C9F: Field Code 2
conf_labels
C9V: Data item to save Values of Field Code 2

Notes

  • To get the API Token, open the App Settings and click “API Token” in the App Settings tab on Kintone.

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

    Do not forget to click “Update App” to apply the update.
  • Guest Space ID (only when the Kintone App is in a guest space) and App ID can be confirmed in the API Token settings on Kintone.
  • Supported field types are: Record Number, Record ID, Revision, Status, Created datetime, Updated datetime, Text, Number, Calculated, Link, Date, Time, Date and time, Lookup, Radio button, Drop-down.
  • See the Kintone Reference for the operators and functions that can be used in Search Query. Query options (order by, limit, offset) are not supported.

Capture

See Also

Script (click to open)
  • An XML file that contains the code below is available to download
    • kintone-record-search.xml (C) Questetra, Inc. (MIT License)
    • If you are using Professional, you can modify the contents of this file and use it as your own add-on auto step


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 bloggers like this: