GAS

GoogleAppsScriptGitHubアシスタント undefinedの解決方法

はじめに

GASとgithubを連携するのに使うChrome拡張機能、
GoogleAppsScriptGitHubアシスタント。便利ですよね。
しかし、そこそこの頻度で以下のようなエラーに遭遇します。
[GitHub assistant] undefined
今回は、このエラーの解決方法について解説していきます。

再認証する

まず1つ目は、再認証する方法です。
Chrome上部ツールバーから拡張機能のボタンを選択、
GitHubアシスタントの・・・からオプションを選択します。
ツールバーのchrome拡張ボタンからオプションを選択

すると、本来はログイン情報に関する画面が表示されます。
が、それと同時に以下のような認証画面がポップアップする場合、
おそらく認証が切れているのでそのままAllowで許可、認証を行ってください。
Googleの認証をAllowする

再ログインする

前項の方法で解決しなかった場合、以下の方法も試してみてください。
前項と同様に上部バーからオプションを開きます。
ログアウトする
ここで一度、ログアウトして再度ログインしてください。
尚、再ログインの際にgithubのアクセストークンが必要になります。

GoogleAppsScriptAPIの設定をオンにする

それでも解決しない場合、
こちらにアクセスして設定をオンにしてください。
GoogleAppsScriptAPIを有効にする

さいごに

Googleアカウントを複数使っていたり、
二段階認証を設定していたりすると、発生頻度が高まるように感じています。
ぜひお役立ていただければと思います。

GAS-AsanaAPI カスタムフィールド指定でタスク生成する方法

はじめに

GASでAsanaAPIを使ってカスタムフィールドありのタスクを作成する際、
調べてもよく分からず暗中模索して困ったため、その答えを紹介します。

gidで指定する必要がある

Asanaの基本的な概念として「gid」というものがあります。
Asanaの内部IDだと認識しておけばOKです。
プロジェクトgidやユーザgidなどが存在します。

そして、カスタムフィールド1つ1つにもgidが存在します。
タスク作成時にカスタムフィールドに値を入力する場合、gidでの指定が必要です。
更にプロジェクト上でカスタムフィールドの選択肢を作成している場合、
その各選択肢にもgidが存在します。

タスク作成、APIでカスタムフィールドを操作する際は、
下記コード例のようにgidを用いて指示します。

function createTask() {
  const headers = {
    "Authorization": "Bearer " + "token"  
  };

  const payload = JSON.stringify({
    "data": {
      "workspace"    : "workspaceId",                            // ワークスペースID              
      "projects"     : ["projectId"],                            // プロジェクトID
      "name"         : "title",                                  // タスクタイトル
      "assignee"     : "assignee@mail.jp",                       // タスク担当者アドレス
      "followers"    : ["collab1@mail.jp", "collab2@mail.jp"],   // コラボレーターアドレス
      "html_notes"   : body,                                     // タスク本文html
      "due_on"       : "2024-03-30",                             // タスク期日
      "custom_fields":{1234567890:"1111111111",   // カスタムフィールドgid:選択肢gid
                       2345678901:"2222222222",   // カスタムフィールドgid:選択肢gid
                       3456789012:"text",         // カスタムフィールドgid:入力値テキスト
                       4567890123:"int"}          // カスタムフィールドgid:入力値数値
    }
  });

  const options = {
    "method"              : "post",
    "contentType"         : "application/json",
    "headers"             : headers,
    "payload"             : payload,
    "muteHttpExceptions"  : true
  };

  const response = UrlFetchApp.fetch("https://app.asana.com/api/1.0/tasks", options);

  if(JSON.parse(response.getContentText()).errors){
    throw new Error(`error:${response.getContentText()}`);
  }
  else{
    const responseJson = JSON.parse(response);
    const task_id  = responseJson.data.gid;
    const taskUrl  = `https://app.asana.com/0/プロジェクトID/${task_id}`;
    return taskUrl;
  }
}

サンプルコードではカスタムフィールド以外にも任意の属性を指定しています。
このコードを使用する場合は、必要に応じて置換してください。

カスタムフィールドのところを見てみると、
gidに対して選択肢のgidや、入力値を直接指定しているのが分かると思います。
選択式フィールドの場合はgidを、入力式フィールドの場合は直指定となります。

カスタムフィールドgidを取得するには

さて、このカスタムフィールドのgidですが、
現在Asanaのweb画面上から取得する方法はありません。

カスタムフィールドのgidを取得するには、
APIのGet a project’s custom fieldsGet a projectを使う必要があります。
前者の方がシンプルに情報取得できるため前者を例示しますが、
カスタムフィールド以外のプロジェクト情報も必要な場合は後者を使うと良いでしょう。

function getProjectCustomField(projectId){
  const baseUrl = `https://app.asana.com/api/1.0/projects/${projectId}/custom_field_settings?limit=100`;
  const options = {
                "method"              : "GET",
                "headers"             : {"Authorization": "Bearer " + token, 
                                        "accept": "application/json"
                                        },
                "muteHttpExceptions"  : true
              };
  let nextPage = null;
  let result = [];

  // ページループ
  do{
    const url = `${baseUrl}${nextPage ? `&offset=${nextPage.offset}` : ""}`;
    const singleResponse = UrlFetchApp.fetch(url, options);
    const jsonResult = JSON.parse(singleResponse);
    result = result.concat(jsonResult.data);
    nextPage = jsonResult.next_page;
  }
  while(nextPage)

  return result;
}

以下のような戻り値となります。
custom_field属性直下のgidがフィールドgidです、
typeがenumの場合、選択式フィールドである事を表しています。
選択肢はenum_optionsに格納されており、その中のgidが各選択肢のgidとなります。

{
  "data": [
    {
      "gid": "12345",
      "resource_type": "task",
      "project": {
        "gid": "12345",
        "resource_type": "task",
        "name": "Stuff to buy"
      },
      "is_important": false,
      "parent": {
        "gid": "12345",
        "resource_type": "task",
        "name": "Stuff to buy"
      },
      "custom_field": {
        "gid": "12345",
        "resource_type": "task",
        "name": "Status",
        "resource_subtype": "text",
        "type": "text",
        "enum_options": [
          {
            "gid": "12345",
            "resource_type": "task",
            "name": "Low",
            "enabled": true,
            "color": "blue"
          }
        ],
        "enabled": true,
        "representation_type": "number",
        "id_prefix": "ID",
        "is_formula_field": false,
        "date_value": {
          "date": "2024-08-23",
          "date_time": "2024-08-23T22:00:00.000Z"
        },
        "enum_value": {
          "gid": "12345",
          "resource_type": "task",
          "name": "Low",
          "enabled": true,
          "color": "blue"
        },
        "multi_enum_values": [
          {
            "gid": "12345",
            "resource_type": "task",
            "name": "Low",
            "enabled": true,
            "color": "blue"
          }
        ],
        "number_value": 5.2,
        "text_value": "Some Value",
        "display_value": "blue",
        "description": "Development team priority",
        "precision": 2,
        "format": "custom",
        "currency_code": "EUR",
        "custom_label": "gold pieces",
        "custom_label_position": "suffix",
        "is_global_to_workspace": true,
        "has_notifications_enabled": true,
        "asana_created_field": "priority",
        "is_value_read_only": false,
        "created_by": {
          "gid": "12345",
          "resource_type": "task",
          "name": "Greg Sanchez"
        },
        "people_value": [
          {
            "gid": "12345",
            "resource_type": "task",
            "name": "Greg Sanchez"
          }
        ]
      }
    }
  ],
  "next_page": {
    "offset": "eyJ0eXAiOJiKV1iQLCJhbGciOiJIUzI1NiJ9",
    "path": "/tasks/12345/attachments?limit=2&offset=eyJ0eXAiOJiKV1iQLCJhbGciOiJIUzI1NiJ9",
    "uri": "https://app.asana.com/api/1.0/tasks/12345/attachments?limit=2&offset=eyJ0eXAiOJiKV1iQLCJhbGciOiJIUzI1NiJ9"
  }
}

さいごに

AsanaAPIにおけるカスタムフィールドへの理解は深まりましたでしょうか。
もし必要があればAsanaに関する他の記事も参照してお役立てください。
Asana APIトークンや各種IDを取得する基本的な方法を解説

GASでGoogleDrive上のJSON内容を取得/上書きする方法

はじめに

Drive上にJSONファイルを置いておき、
それをGASから操作する際の基本的な方法を紹介します。

なぜGASでJSONファイルを使うのか

本文の前に、なぜこんなことをするのかについて。
社用webアプリ等に使う半端な量のデータを持つのに有用だからです。

社用とは言えwebアプリケーションですから、
操作すべき何らかの情報や、操作した結果情報を保存する必要があります。
これらをもし全てスプレッドシート上で管理したら、どうなるでしょうか。
web上の操作時にスプレッドシート上の数千行のデータを処理して・・・
となると、20秒とか30秒とかかかります。
webアプリを操作している時の読込みで30秒は流石に酷いですよね。
データ量が本当に多いならBigQueryなどデータベース運用が視野に入りますが、
DB使う程ではないけどシート管理も良くないという時、Drive上にJSONを置いておくのも1つの方法です。

読込のコード

以下のコードでJSONとして扱えます。

const file = DriveApp.getFileById("id");
const jsonStr = file.getBlob().getDataAsString("UTF-8");
const json = JSON.parse(jsonStr);

書き込み(上書き)のコード

以下のコードで書き込みできます。
drive上のテキストファイルの扱いとほぼ同じですね。
上書きとなりますので、差分のみ変更した場合はそのようなデータを予め作成する必要があります。

const file = DriveApp.getFileById("id");
file.setContent(JSON.stringify(json));

最後に

コード自体はごくシンプルですが、扱いようによっては非常に便利です。
いかなる時にドライブ上のJSONデータを運用するのかという点が重要だと思います。
getValuesが遅すぎると悩んでいる方は一度試してみてください。

BigQueryのJSONスキーマとJSON形式ロードジョブの扱い

はじめに

BigQueryを扱う際、JSONに関する情報が少なく
非常に苦労したため、ノウハウを共有します。
尚、一部例示するコードはGASです。
またJSONはいわゆる一般的なオブジェクト配列を指します。

 

事前準備

検証に使うテーブルに、JSON型のカラムを用意してください。
また、サンプルコードを実行したい場合は、
GASのエディタを開いてBigQueryサービスを有効化しておいてください。

 

JSONカラムへのINSERT・UPDATE

まず、基本的なクエリの書き方はこんな感じです。

"UPDATE `projectId.datasetId.tableId` 
 SET カラム名 = JSON'ここにJSONstring'
 WHERE id = '12345"

JSONstringに”JSON”を接頭する必要があります。

GASで作成する場合はシンプルにJSON.stringifyを使えばいいです。

"UPDATE `projectId.datasetId.tableId` " + 
` SET カラム名 = JSON'${JSON.stringify(jsonData)}' WHERE id = '12345" `;

JSON形式でのloadジョブ登録

こちらは通常のクエリリクエストではなくloadジョブの方法となります。
例えばテーブルのカラムが「name(string),id(string),info(json)」だったとして、
以下のようなJSONを一気に取り込む事が可能です。

[
  {
    name:"山田太郎",
    id:"0001",
    info:[{
            type:"parttime",
            sex:"female",
            tel:"111-1111-1111",
         }]
  },
  {
    name:"山田花子",
    id:"0002",
    info:[{
            type:"fulltime",
            sex:"male",
            tel:"222-1111-1111",
         }]
  },
]

loadジョブのイメージは湧きましたでしょうか。
テーブルに初期データを一気に格納する際などに便利です。

さて、ではloadジョブの作成方法です。
上述のカラム構成とJSONを使うとして、以下のようなGASコードとなります。
このコードを実行するとJSONの内容が一括で登録されます。
尚、データ量の制約などもありますので、必要に応じて分割実行してください。

const json = 省略 上述のJSONがあるものとする;const schema = [
  {name: "name", type: "STRING"},
  {name: "id", type: "STRING"},
  {name: "info", type: "STRING"}
];
const jobInfo = {
  configuration: {
    load: {
      destinationTable: {
        projectId: projectId,
        datasetId: datasetId,
        tableId: tableName
      },
      schema: {
        fields: schema
      },
      sourceFormat: 'NEWLINE_DELIMITED_JSON'
    }
  }
};
const jsonl = json.map(x => JSON.stringify(x)).join('\n');
const blob = Utilities.newBlob(jsonl);
const job = BigQuery.Jobs.insert(jobInfo, dbSetting.projectId, blob);

最も重要なポイントは、JSONの加工ですね。
JSONLという改行区切JSON形式に変換しています。

ちなみにロードジョブの結果受取はこんな感じでいいかなと。

function getLoadJobResult(job){
  const projectId = job.jobReference.projectId;
  const jobId = job.jobReference.jobId;
  let result = BigQuery.Jobs.get(projectId, jobId);

  let count = 0;
  while (result.status.state !== "DONE") {
    console.log(result);
    console.log("loadResponse待ち...")
    if(count==10) throw(`GetLoadJobResult10回失敗${result}`);
    Utilities.sleep(2000);
    result = BigQuery.Jobs.get(projectId, jobId); 
    count++;
  }
  if(result.status.errors) console.log(result.status.errors);
  return result;
}

前回記事で紹介したクラス内の待ち受けと一緒ですね。

さいごに

BigQueryにおけるJSON関連の扱いについて、理解は深まりましたでしょうか?
前回の記事ではGASでBigQueryを扱う際のクラス等も紹介しています。
ぜひあわせてお役立て頂けると幸いです。

GASでBigQueryにジョブ登録&結果取得する基本的な方法

はじめに

GASからBigQueryを実行する時の基本的なコードを紹介していきます。
プロジェクトやテーブルの作成方法は色んなサイトで既に解説されていますので、
BigQueryでプロジェクト及びテーブルが作成されている事を前提とします。
GASエディタにBigQueryサービスを追加しておくのも忘れないでください。

 

ジョブ登録

const job = BigQuery.Jobs.query(
  {
    useLegacySql: false,
    query: "SELECT * FROM `projectId.datasetId.tableName`",
    timeoutMs: 20000,
  },
  projectId
);

上記のコードが基本的なジョブ登録の方法になります。
このコードを実行すると、BigQueryプロジェクトに実行ジョブとして登録されます。

projectId、datasetId、tableNameの部分については、
実行対象のテーブル情報をBigQueryから取得して入力してください。
projectId.datasetId.tableNameの部分はバッククオートで囲む必要があります。
リテラルや変数を使う場合は若干書きにくいので注意してください。

特筆すべき注意点として、console.log(job)しても結果が得られない点です。
当コードで作成したジョブ情報を元に、BigQueryへ結果を取りに行く必要があります。

 

ジョブ結果取得

//前項で登録したjob変数が存在するものとする
const projectId = job.jobReference.projectId;
const jobId = job.jobReference.jobId;
const location = job.jobReference.location;
const result = BigQuery.Jobs.getQueryResults(projectId, jobId, {location: location});

これがジョブ結果取得の基本的な方法となります。
ただし、単にこれを書くだけでは、いくつか問題があります。

まず、実行時間の考慮が必要になるという点です。
実行するクエリによっては所要時間がかかるものがあります。
ジョブ登録してすぐに上記コードを実行すると、
「未完了だよ」という結果が返ってくる場合があります。
ですので、後述しますが、待ち受け処理が必要です。

また、返ってきた結果の処理も必要です。
result.statusで成否取得できるため、これでエラー処理をすべきです。
またselect等で取得した結果はresult.rowsで取得できますが、
シンプルな形式ではないので扱いやすいJSON等に変換する必要があります。

 

改善案

上述のコードをより実務的にする案です。

class BigQueryClass{
  /**
   * @param {object} dbSetting {projectId:"projectId", datasetId:"datasetId"}
   */
  constructor(dbSetting){
    this.ProjectId = dbSetting.projectId;
    this.DatasetId = dbSetting.datasetId;
  }

  /**
   * projectIDとdatasetIDとtable名を連結して返す 
   * @return {string} "`projectId.datasetId.tableName`"
   */
  GetIdsStr(tableName){
    const ids = `${this.ProjectId}.${this.DatasetId}.${tableName}`;
    return "`" + ids + "`";
  }

  /**
   * 取得系 実行して結果を取得する
   * @param {string} queryStr
   * @return {object} {queryStr:str, json:結果オブジェクト配列} 
   */
  ExecuteGetQuery(queryStr){
    const result = this.executeQuery(queryStr, true);
    return result;
  }

  /**
   * 更新系 実行する
   * @param {string} queryStr SQL文
   * @return {object} {queryStr:str, json:[空配列]}
   */
  ExecuteChangeQuery(queryStr){
    const result = this.executeQuery(queryStr, false);
    return result;
  }

  /**
   * UPDATE文など競合不可な実行をロックしながら実行する
   * @param {string} queryStr 実行SQL文
   * @param {int} lockTime ロック待機時間ミリ秒
   */
  ExecuteQueryWithLock(queryStr, lockTime=30000){
    const lock = LockService.getScriptLock();
    if(lock.tryLock(lockTime)){
      try{
        const result = this.executeQuery(queryStr, false);
        return result;
      }
      catch(e){
        console.log("UPDATE実行Error" + queryStr);
        throw e
      }
      finally{
        lock.releaseLock();
      }
    }
    else{
      console.log("ロックNG");
      throw new Error("ExecuteQueryWithLock:ロック獲得不可");
    }
  }

  /**
   * 実行して結果を取得する
   * @param {string} queryStr
   * @param {bool} isGet select等の取得系:true  update等の更新系:false
   * @return {object} {queryStr:str, json:結果オブジェクト配列} 
   */
  executeQuery(queryStr, isGet){
    const job = this.registQueryJob(queryStr);
    const result = this.getJobResult(job);
    const jsonResult = this.getJsonFromQueryResult(queryStr, result, isGet);  //{queryStr,json}

    return jsonResult;
  }


  /**
   * ジョブを登録する
   * @param {string} queryStr
   * @return {object} ジョブ情報
   */
  registQueryJob(queryStr) {
    try{
      let job = BigQuery.Jobs.query(
        {
          useLegacySql: false,
          query: queryStr,
          timeoutMs: 20000,
        },
        this.ProjectId
      );
      return job;
    }
    catch(e){
      console.log(e.message);
      throw(`BigQueryジョブ登録Error:${e.message}`);
    }
  }

  /**
   * ジョブの結果を待つ
   * @param {object} job ジョブ情報
   * @return {object} SQL実行レスポンス
   */
  getJobResult(job){
    try{
      const projectId = job.jobReference.projectId;
      const jobId = job.jobReference.jobId;
      const location = job.jobReference.location;
      let result = BigQuery.Jobs.getQueryResults(projectId, jobId, {location: location});

      // 実行結果待ち
      let count = 0;
      while (!result.jobComplete) {
        console.log(result);
        console.log("response待ち...");
        if(count==10) throw(`GetJobResult10回失敗${result}`);
        Utilities.sleep(2000)
        result = BigQuery.Jobs.getQueryResults(projectId, jobId, {location: location});
        count++;
      }
      if(result.status){
        if(result.status.errors){
          throw(result.status.errors);
        }
      }
      return result;
    }
    catch(e){
      throw(`BigQueryジョブ結果取得Error:${e.message}`);
    }
  }

  /**
   * 実行responseを結果objに変換する
   * @param {string} queryStr 
   * @param {object} response 
   * @param {bool}   isGet    select等の取得系:true  update等の更新系:false
   * @return {object} {queryStr:str, json:結果オブジェクト配列}
   */
  getJsonFromQueryResult(queryStr, response, isGet){
    try{
      let ret;
      switch(isGet){
        case true:  //取得系
          if(response.totalRows == "0")
           return {queryStr:queryStr, json:[]};

          const rows = response.rows;
          const schema = response.schema;
          const jsonData = rows.map(row => {
            const obj = {};
            row.f.forEach((field, i) => {
              obj[schema.fields[i].name] = field.v; 
            });
            return obj;
          });

          ret = {queryStr:queryStr, json:jsonData};
          return ret;

        case false: //更新系
          ret = {queryStr:queryStr, json:null};
          return ret;
      }
    }
    catch(e){
      throw(`BiqQuery結果変換Error:${e.message}`)
    }
  }
}

そのまま使う場合、クラスに関して一定の理解がないと厳しいかもしれません。
ただ断片的にでも切り出せる部分はあると思います。

改善したポイントは以下の通りです。

  • projectId.datasetId.tableNameを生成しやすくした
  • 結果取得時の待機を考慮
  • 結果取得時のエラー処理を追加
  • 更新系と取得系で処理結果の取得を分岐
  • 取得系の結果をシンプルなJSONに変換

 

実際に使う際は、インスタンスを生成してメソッドを呼び出します。
メソッド名が大文字で始まるものを使います。
小文字のものは内部用です。(他言語におけるprivateメソッドのイメージ)

const cls = new BigQueryClass({projectId:"projectId", datasetId:"datasetId"});
const query = `SELECT columnName FROM ${bqCls.GetIdsStr("tableName")} 
                  WHERE id = '123456'`;
const json = bqCls.ExecuteGetQuery(getQuery).json;

 

最後に

BigQueryにはBigQueryなりのクセがあり苦労したため、公開しました。
他にも苦労したポイントとしてBigQueryでのJSONの扱いについて記事を作成しています。
BigQueryのJSONスキーマとJSON形式ロードジョブの扱い
ぜひ実装のヒントにして頂けますと幸いです。

GASの変更時トリガーが実行頻度によっては実行されない問題について検証してみた

0.はじめに

GASの変更時トリガーってあるけど、
本当に全ての変更をキャッチしてくれてるの?

と疑問に思ったので検証してみました。

例えばGoogleフォーム回答が入力されるスプレッドシートに対して、
シート変更時トリガーを仕込んでおり複数から同時に回答があったとしたら・・・?
本当はそんなスクリプトは組まない方がいいわけですが、
そんな時どうなるのか気になる方はぜひご覧ください。

 

1.検証用のコードとトリガー

本当の限界頻度を知りたいので、ごくシンプルな負荷の低いコードを使いました。


function myFunction() {
  console.log("testやで");
  GmailApp.createDraft("test@test","testやで","testやわ");
}

トリガーはこちら。普通の変更時トリガーです。
変更時トリガー設定の画像
もちろんこのスクリプトはスプレッドシートにバインドしています。

 

2.高速で実行してみた

まず、適当に10回実行してみた結果がこちらです。
セルを10個変更したスプレッドシートの画像
6行の実行ログ
10回変更したのに実行ログは6行しかあらへん。どういうこっちゃ。
実行されてるけど、実行ログだけ出てないのか?と思い、Gmailの下書きを確認します。
6件だけのメール下書き
やっぱり6件しかない。という事で、6回しか実行されていないようです。
高速での変更時トリガーは実行が欠損する
という事が分かりました。
(1回だけでは試行回数不足のため複数回、別時期にも試しております。)

 

3.実行頻度を落として実行してみた

どれぐらいが敷居なのか判断したいため、
秒間1~3回程度に実行ペースを落としてみます。

10回成功 秒間1~2回
10回成功した画像

10回成功 秒間1~3回
10回成功した画像

9回成功 秒間1~2回
9回成功した画像

無暗に高速実行した時より成功率は上がりましたが、まだムラがあります。
(3回だけでは試行回数不足のため複数回、別時期にも試しております。)

 

4.秒間1回にしてみた

では秒間1回ではどうでしょうか。

秒間1回程度で10回成功
秒間1回程度の実行で10回成功している画像

秒間1回程度で10回成功その2
秒間1回程度の実行で10回成功している画像

秒間1回程度で10回成功その3
秒間1回程度の実行で10回成功している画像

概ね、1秒に1回程度の実行であれば安定して動きそうです。
私の過去の検証や経験則を合わせても、
1秒1回以下のペースで安定すると結論付けてよいのでは、と考えています。

 

5.さいごに

いかがでしたでしょうか。
変更時トリガーが安定しない、同時実行の際に不安、
といった場合にはぜひこの内容を思い出してみてください。

それにしても、皆さんに画像で伝わるように秒間1回で実行するとかって難しいですね…w
GASの限界に挑戦するシリーズ、個人的に面白いなと思っているので、
何か思いつけばまたやってみようと思います。
「これ試してみてほしい!」等あればお気軽にコメントくださいね。

フォームのデータを取得できませんでした。に対処したもう1つのシンプルな方法

0.はじめに

以前、こんな記事を書きました。
「フォームのデータを取得できませんでした。のエラーに対処した話」

内容としては、おそらくGoogle側の問題が原因で発生するエラー、
Exception: フォームのデータを取得できませんでした。しばらくしてからもう一度お試しください。
に対処する方法についてでした。
GASのフォーム送信時トリガーで回答情報を取得しようとする際に起こるエラーですね。

ただ、エラーを防止する方法は無く、
エラーが起きても後から再実行できる方法を提案するものでした。

記事の投稿から時間が経ち、私が採用しているコードも変わりました。
相変わらずエラーを防止する方法はありませんが、
上記の記事はいまだに結構アクセスがありますので、
最新版をお届けしようと思います。

 

1.コード

// フォーム送信時トリガー
function main(e) {
  let datas = {status:false, msg:"初期値"};
  //フォームデータ取得を20回トライする
  for(let i = 1; i<=20; i++){
    datas = getFormDatas(e);
    if(datas.status) break;
    Utilities.sleep(3000);
  }
  if(!datas.status){
    //前回記事の方法やエラーメール送信などを書く
  };
}

//フォームデータの取得
function getFormDatas(e){
  try{
    // 必要な処理をする
    const formTitle = FormApp.getActiveForm().getTitle();
    const itemresponses = e.response.getItemResponses();
    const address = e.response.getRespondentEmail();
    const responseId = e.response.getId();
    return {status:true,formTitle:formTitle,itemresponses:itemresponses,address:address,category:category,responseId:responseId};
  }
  catch(er){
    console.log(er);
    return {status:false, msg:er.message};
  }
}

 

2.簡易解説

フォーム回答を取得する部分について、getFormDatasという関数にしています。
これを、成功するまで20回を上限に実行しています。ごくシンプルですね。
高速でループしてもサーバーエラーが解消しない限り無意味なので、
気分的にループ毎に3秒のwaitを入れています。
このwaitに本当に意味があるかは分かりません!

 

3.検証結果

このコードを書いた当時、SeleniumでGoogleフォームを1,500回、回答してテストしました。
※seleniumとは、ブラウザを自動操作するツールです。pythonやC#で使えます。
エラーが出ない時期もありますが、出る時期に行いました。
結果、2回以上のループに入るケースが2割ほどありましたが、
20ループを超えてエラーを吐くケースは1%未満に留まりました。

 

4.ループ回数は何回が適切なのか

ループしすぎて処理がタイムアウトにならなければいいのではないでしょうか。
あまり気にせず適当に決めていいと思いますが、
エラーが出る時期は5ループとか10ループだと結構突破してくる印象があります。

 

5.最後に

いかがでしたでしょうか。
単に試行回数を増やすという強引な対処ですが、
ランダム性のあるこの手のエラーには1つの有効な手立てとなります。
ぜひ、前回記事と合わせて活用してみてください。

AdminDirectoryでエイリアスを追加/削除する方法 – GAS

0.はじめに

エイリアスを操作しようと思っていざ調べてみると、
思いのほか情報が少なく、誤情報もあり困ったため、方法を共有します。

 

1.追加する方法


AdminDirectory.Users.Aliases.insert({"alias":"~~~~@~~~~~"}, "userMail@~~~~");
//AdminDirectory.Users.Aliases.insert({"alias":"エイリアスメールアドレス"}, "ユーザのプライマリメールアドレス");

 

2.削除する方法


AdminDirectory.Users.Aliases.remove("userMail@~~~~", "エイリアス@~~~~");
//AdminDirectory.Users.Aliases.remove("ユーザのプライマリメールアドレス", "エイリアスメールアドレス");

 

3.一括更新できないのか?

色々と検証してみたところ、ループを回すしか方法が無さそうです。
メソッド1発でできる方法があれば是非教えてください。

AdminDirectoryで管理者が自アカウントを変更すると403になる罠

0.はじめに

GoogleAppsScriptでAdminDirectory系のメソッドを実行した際に403が出て若干ハマったので共有します。

 

1.結論

結論:管理者アカウントを操作しようとしたからです。

 

通常、AdminDirectoryServiceはそれなりの権限がないと実行できません。
なぜならユーザー情報やグループ情報を変更したり削除したりできてしまうからです。
「運用中の通常ユーザーの情報は触りたくないから自分のアカウントで試すか~」
とやると、403が返ってきます。
あなたのアカウントは管理者アカウントではありませんか?
AdminユーザーでもAdminユーザーを変更する事ができないようです。


API call to directory.users.aliases.insert failed with error: Not Authorized to access this resource/api

「AdminDirectory 403」等で調べると、スコープや権限の話が出てきますが、
スコープやユーザー変更権限があっても実行できません。
「Groups.list()とかUsers.insert()とかできるのになんで?」となりますが、
管理者アカウントは操作できる対象ではありません。

 

2.ちなみに実行したコードはこちら

ちなみに私が実行したコードはこれでした。
自分のメールエイリアスを設定しようとしたものです。
Users.Aliasesの情報が少なく「何かコードがおかしいんだ」と悩みましたが、
変更対象アカウントを一般ユーザーにしたところ普通に動作しました。


AdminDirectory.Users.Aliases.insert({"alias":"~~~~@~~~~~"}, "userMail@~~~~");
AdminDirectory.Users.Aliases.remove(userKey, "userMail@~~~~");

GASでドライブに日毎、月毎のフォルダを一気に作成する方法【コピペでOK】

0.はじめに

ドライブに日毎のフォルダを一気に作成したい!
ということをGASで実現する記事です。

ちまちま作成している人はパパっとコピペして活用しちゃってください!

1.フォルダ作成のコード

//エディタ左からライブラリday.jsを追加してください。スクリプトIDは下記。
//1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
function main() {
  //親フォルダを定義
  const rootFolder = DriveApp.getFolderById("親フォルダID");
  let monthFolder = "";

  //開始日と最終日を設定
  let targetDate = dayjs.dayjs("2023/1/1");
  let endDate = dayjs.dayjs("2024/12/31");

  //最終日を超えるまでtargetDateを加算しつつ繰り返し処理
  while(targetDate.isBefore(endDate.add(1,"day"),"day")){
    //日付が1日or月フォルダ未指定の場合はフォルダ作成(root内に当月フォルダが存在するか確認するとよりgood)
    if(targetDate.date()==1 || monthFolder==""){
      //月フォルダを作成
      monthFolder = rootFolder.createFolder(targetDate.format("YYYY/MM"));
    }
    //月フォルダ内に日フォルダを作成
    monthFolder.createFolder(targetDate.format("YYYY/MM/DD"));
    //targetDateに1日加算
    targetDate = targetDate.add(1,"day");
  }
}

コード内にコメントで書いている通り、day.jsを使用するコードになっています。
エディタ左側のライブラリのところから、追加してください。

day.jsのスクリプトIDはこちら
1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
day.js公式

実行結果としてはこんな感じになります。
↓↓↓

親フォルダ内に月フォルダが並び・・・

生成済みの月フォルダたち

月フォルダの中には日毎のフォルダが並びます。

月フォルダ内に日毎のフォルダが並ぶ画像

尚、注意点ですが、少し処理時間を要します。
なぜなら1つずつフォルダを順次作成していくからです。

上記の例、丸2年分を私の環境で実行すると、2分15秒かかりました。
手で作成するよりは断然速いですが、あまり大量に作成するとタイムアウトしますので
ある程度は区切って処理していくことをオススメします。

2.当日になったらフォルダを削除したい、そんな時のコード

作って使うだけなら楽ですが、管理上そうもいかない場合が多いです。
そんな時、日次で夜実行のトリガーを設定しておいて当日分を削除するようなコードがあれば便利ですよね。
それがこちらです。月末日の場合は月フォルダも削除します。


//本日のフォルダを削除
function deleteTodayFolder(){
  const rootFolder = DriveApp.getFolderById("親フォルダID");
  const today = dayjs.dayjs();
  const monthFolder = rootFolder.getFoldersByName(today.format("YYYY/MM")).next();
  
  if(monthFolder){
    //月フォルダが見つかったら本日フォルダを検索し、見つかったら削除する
    let todayFolder = monthFolder.getFoldersByName(today.format("YYYY/MM/DD")).next();
    if(todayFolder) todayFolder.removeFolder();

    //翌日が1日だったら月末なので、月フォルダも削除する
    if(today.add(1,"day").date()==1) monthFolder.removeFolder();
  }
}

こちらもday.jsを使いますので事前に追加しておいてくださいね。

3.昨日分を削除したい方へのコード

別に当日分を23時~24時のトリガーで日次削除してもいいけど、
できれば厳密に日が変わってから昨日分を削除したい方はこちらをどうぞ。


//昨日のフォルダを削除
function deleteYesterdayFolder(){
  const rootFolder = DriveApp.getFolderById("親フォルダID");
  const yesterday = dayjs.dayjs().subtract(1,"day");
  const monthFolder = rootFolder.getFoldersByName(yesterday.format("YYYY/MM")).next();
  
  if(monthFolder){
    //月フォルダが見つかったら昨日フォルダを検索し、見つかったら削除する
    let yesterdayFolder = monthFolder.getFoldersByName(yesterday.format("YYYY/MM/DD")).next();
    if(yesterdayFolder) yesterdayFolder.removeFolder();

    //本日が1日であれば先月の月フォルダも削除する
    if(dayjs.dayjs().date()==1) monthFolder.removeFolder();
  }
}

こちらもday.jsです。便利ですね。
私が実務で使っているのも、このパターンです。

4.さいごに

いかかでしたでしょうか。結構シンプルなコードだと思いませんか?
これだけのコードですが、手で作業するとなると莫大な時間を要します。

ぜひGASを活用して効率化、高速化を実現してきましょう!
ご活用いただけると幸いです。それではまた。