【JS】JS で ファイル出力するCSVデータ生成ツールを作ってみる

■ はじめに

https://dk521123.hatenablog.com/entry/2022/11/23/000000

の続き。

 最近、プログラムをほとんど書いていないので
上記の関連記事の「【2】課題」の部分を修正して
もう少し発展したツールを作ってみた。
ついでに、ファイル出力もやってみたので、メモ。

目次

【1】JavaScriptによるファイル出力
 1)補足:Blobについて
【2】サンプル

【1】JavaScriptによるファイル出力

* 以下のサイトなどに載っている

https://magazine.techacademy.jp/magazine/28206

サンプルより抜粋

function outputFile() {
  const textArea = document.getElementById("testData");
  const csvData = textArea.value;
  const csvArray = csvData.split('');
  const blob = new Blob(csvArray, {type:"text/plan"});

  let link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = "output.csv"
  link.click();
}

1)補足:Blobについて

* BLOB(Binary Large Object)を扱うためJavaScriptのオブジェクト

https://developer.mozilla.org/ja/docs/Web/API/Blob

構文

const blob = new Blob(データ, ファイルタイプ);

【2】サンプル

<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<form id="mainForm">
  <div id="div0">
    <textarea id="testData" name="testData" rows="6" cols="100"></textarea><br />
    <input id="delimiter"" name="delimiter" maxlength="20" size="20" value=","><br />
    <input id="maxRow" name="maxRow" maxlength="20" size="20" value="5"><br />
    <button type="button" id="outputFile">ファイル出力</button>
  </div">
  <hr>
  <div id="div1">
    <input id="item1" name="itemName1" class="inputText" type="text" maxlength="20" size="20" placeholder="Enter your header item">
    <select name="dataTypeName1" id="dataType1">
      <option value="String" label="String"></option>
      <option value="Number" label="Number"></option>
      <option value="Boolean" label="Boolean"></option>
    </select>
  </div>
</form>
</body>
<script>
var index = 1;

function canAddInput() {
  var canAdd = true;
  console.log("Check input values");
  const inputTexts = document.querySelectorAll(".inputText");
  for(let inputText of inputTexts) {
    console.log(`Value = ${inputText.value}`);
    if (!inputText.value) {
      canAdd = false;
      break;
    }
  }
  return canAdd;
}

function addRow() {
  if (!canAddInput()) {
    console.log("No need to add inputs any more");
    return;
  }
  
  console.log("Need to add input!!");
  index = index + 1;
  const newDiv = document.createElement("div");
  newDiv.setAttribute("id", `div${index}`);
  
  const newInput = document.createElement("input");
  newInput.setAttribute("id", `item${index}`);
  newInput.setAttribute("name", `itemName${index}`);
  newInput.setAttribute("class", "inputText");
  newInput.setAttribute("type", "text");
  newInput.setAttribute("maxlength", "20");
  newInput.setAttribute("size", "20");
  newInput.setAttribute("placeholder", "Enter your header item");
  newInput.setAttribute("value", "");
  newInput.addEventListener('blur', addRow);
  newInput.addEventListener('input', createData);

  const newSelect = document.createElement("select");
  newSelect.setAttribute("name", `itemTypeName${index}`);
  newSelect.setAttribute("id", `dataType${index}`);
  newSelect.addEventListener("change", createData);

  const optionList = ["String", "Number", "Boolean"];
  for (optionType of optionList) {
    const newOption = document.createElement("option");
    newOption.setAttribute("value", optionType);
    newOption.setAttribute("label", optionType);

    newSelect.appendChild(newOption);
  }
  
  const mainForm = document.getElementById("div0");
  mainForm.appendChild(newDiv);
  newDiv.appendChild(newInput);
  newDiv.appendChild(newSelect);
}

function createData() {
  const maxRow = document.getElementById("maxRow").value;
  const delimiter = document.getElementById("delimiter").value;
  console.log(`Start to create data. ${maxRow} / ${delimiter}`);

  const inputTexts = document.querySelectorAll(".inputText");
  var headers = new Array();
  var dataTypes = new Array();
  for (var i = 0; i < inputTexts.length; i++) {
    var inputText = inputTexts[i];
    console.log(`Value = ${inputText.value}`);
    if (!inputText.value) {
      continue;
    }
    headers.push(inputText.value);
 
    var targetIndex = i + 1;
    var targetSelect = document.getElementById(`dataType${targetIndex}`);
    console.log(`DataType = ${targetSelect.value}`);
    dataTypes.push(targetSelect.value);
  }
  if (headers.length === 0) {
    console.log("No data");
    return;
  }
 
  console.log(`Creating... ${headers}`);
  
  var result = headers.join(delimiter) + "\n";
  for (var i = 0; i < maxRow; i++) {
    for (var j = 0; j < headers.length; j++) {
      if (j !== 0) {
        result = result + delimiter;
      }
      const dataType = dataTypes[j];
      var dataValue = "";
      switch (dataType) {
        case "Number":
          dataValue = 1 + Math.floor(Math.random() * 100 );
          break;
        case "Boolean":
          dataValue = Math.random() < 0.5;
          break;
        case "String":
        default:
          dataValue = Math.random().toString(36).substring(2, 12);
          break;
      }

      result = result + dataValue;
    }
    result = result + "\n";
  }
  
  console.log(`Created... ${result}`);
  const textArea = document.getElementById("testData");
  textArea.value = result;
}
function outputFile() {
  const textArea = document.getElementById("testData");
  const csvData = textArea.value;
  const csvArray = csvData.split('');
  const blob = new Blob(csvArray, {type:"text/plan"});

  let link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = "output.csv"
  link.click();
}

const inputText = document.getElementById("item1");
inputText.addEventListener('blur', addRow);
inputText.addEventListener('input', createData);

const outputButton = document.getElementById("outputFile");
outputButton.addEventListener('click', outputFile);
</script>
<html>

出力例

id,name,age,sex,remarks
3tu6vvv23h,kekbwn1g7a,26,false,14jcme128e
32vi64hozk,cjnefypat3,69,false,i1uwhcz881
oqw2luy37x,qwtrviddu1,68,true,svlf10blz1
b84zxuddct,m1z0so2uk8,12,false,bqv81v2rcu
u1apn4halt,h635vg0z6g,2,false,biwqf47upi

関連記事

JS で テスト用の超簡易なCSVデータ生成ツールを作ってみる
https://dk521123.hatenablog.com/entry/2022/11/23/000000
動的に生成したinputタグを考える
https://dk521123.hatenablog.com/entry/2022/11/24/230044
DOM ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2011/01/07/012520
DOM ~ 基本編 / オブジェクト取得 ~
https://dk521123.hatenablog.com/entry/2022/11/21/000000
DOM ~ 基本編 / オブジェクト作成 ~
https://dk521123.hatenablog.com/entry/2022/11/19/000000
DOM ~ 基本編 / オブジェクト操作 ~
https://dk521123.hatenablog.com/entry/2011/01/09/155824
JavaScriptでゼロ埋め
https://dk521123.hatenablog.com/entry/2022/11/01/000000

【Snowflake】【トラブル】エラー「Error assuming AWS_ROLE」時の対応

■ はじめに

長い間、よくわからなかったSnowflakeの接続問題について
やっと原因が理解できたので、メモする。

目次

【1】トラブル概要
【2】エラー内容
【3】原因
 1)確認方法
【4】解決案

【1】トラブル概要

Snowflakeから、Integration(ストレージ統合)を使って
ステージ経由で、AWS S3上のファイルにアクセス(※)しようとした際に
以下「エラー内容」が表示された。

※ 以下「SQL例」の「[3] 接続テスト用として、SELECT」を参照

SQL

-- [1] INTEGRATION の作成
CREATE STORAGE INTEGRATION demo_integ
    TYPE = EXTERNAL_STAGE
    STORAGE_PROVIDER = S3
    ENABLED = TRUE
    STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::xxxx:role/your_role'
    STORAGE_ALLOWED_LOCATIONS = ('s3://your-s3-bucket/test/')
;

-- [2] ステージ作成(ここまでOK)
CREATE OR REPLACE TEMPORARY STAGE demo_stage
  URL = 's3://your-s3-bucket/test/demo/'
  STORAGE_INTEGRATION = demo_integ
  FILE_FORMAT = (TYPE =PARQUET COMPRESSION = SNAPPY)
;

-- [3] 接続テスト用として、SELECT (★ここでエラー★)
SELECT * FROM @demo_stage LIMIT 10;

【2】エラー内容

SQL execution error: Error assuming AWS_ROLE.
Please verify the role and externalId are configured correctly
 in your AWS policy.

和訳

SQL 実行エラー: エラー assuming AWS_ROLE(※)
あなたのAWSポリシー内で、
そのロールおよび外部ID(externalId)が正しく設定されているか
を確認してください。

cf. assume = 引き受ける
cf. verify = 確認する、検証する

※ AssumeRole については、以下のサイトなどを参照

https://dev.classmethod.jp/articles/iam-role-and-assumerole/
https://oji-cloud.net/2019/08/16/post-2701/

 => どこをどうやって確認するのかは、後述「確認方法」を参照のこと

【3】原因

AWS Role 側で、作成したINTEGRATION の
Trust Relationship(信頼関係)が設定されていなかったため。

1)確認方法

* 以下を一読してみるのもいいかも。

https://docs.snowflake.com/ja/user-guide/data-load-s3-config-storage-integration.html

Step1: STORAGE_AWS_EXTERNAL_ID を確認する

-- Step1
-- 「DESC INTEGRATION 【ストレージ統合名】」で
-- STORAGE_AWS_EXTERNAL_ID(信頼関係を確立するために必要な外部 ID)
-- を確認する
DESC INTEGRATION demo_integ;

<出力結果・一部抜粋>
| STORAGE_AWS_IAM_USER_ARN  | String        | arn:aws:iam::123456789001:user/abc1-b-self1234
| STORAGE_AWS_ROLE_ARN      | String        | arn:aws:iam::xxxx:role/your_role
| STORAGE_AWS_EXTERNAL_ID   | String        | MYACCOUNT_SFCRole=2_a123456/xxxxxxxxxxx=

Step2: STORAGE_AWS_EXTERNAL_ID を確認する

[1] AWS Managementコンソールにログイン

[2] [IAM]-[Roles]を選択していき、Textboxに指定されているロールを入力
 => 今回の場合「arn:aws:iam::xxxx:role/your_role」

[3] 指定しているロールのリンク押下し、[Trust Relationship]タブを選択
 => そこで Step1で確認した「STORAGE_AWS_EXTERNAL_ID」が含まれているかどうかを確認

Trust Relationshipの例

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789001:user/abc1-b-self1234" <= snowflake_user_arn
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "ACCT_SFCRole=2_zzzzzzzzz" <= ★snowflake_external_id「MYACCOUNT_SFCRole=2_a123456/xxxxxxxxxxx=」がない
        }
      }
    }
  ]
}

【4】解決案

* 作成したIntegration(ストレージ統合)の 
 STORAGE_AWS_EXTERNAL_ID を追記する
 => 開発環境などで汎用的にしたい場合は、
  「"MYACCOUNT_SFCRole=*」など「*」を使って指定するのもありかも。

Trust Relationshipの修正例

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789001:user/abc1-b-self1234"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": [
             "ACCT_SFCRole=2_zzzzzzzzz",
             "MYACCOUNT_SFCRole=2_a123456/xxxxxxxxxxx=" <= ★ここを追加★
          ]
        }
      }
    }
  ]
}

注意点

 Integration(ストレージ統合)を再作成すると
STORAGE_AWS_EXTERNAL_IDが変更されるので、一度設定した際は、
DROP & CREATE や CREATE OR REPLACE 等で再作成しないように注意すること
 => 変更する場合は、ALTER で...

関連記事

Snowflake ~ 基礎知識編 ~
https://dk521123.hatenablog.com/entry/2021/11/02/130111
Snowflake ~ 入門編 / Hello world
https://dk521123.hatenablog.com/entry/2021/11/22/212520
Snowflake ~ 基本編 / ステージ ~
https://dk521123.hatenablog.com/entry/2022/09/01/220643
Snowflake ~ ストレージ統合 ~
https://dk521123.hatenablog.com/entry/2022/06/29/221037
エラー「Failure using stage area. ... AccessDenied」時の対応
https://dk521123.hatenablog.com/entry/2022/10/27/195547

【JS】動的に生成したinputタグを考える

■ はじめに

CSV/TSVテストデータを自動的に作るツールを作ることを考えている(※)。
個人的に、ツールを作る際に、一番汎用性があるのは、
JavaScriptではないかと。

 そこで、AWSマネージメントコンソールのような
値が入ってたら、動的に Textboxが生成するようなサンプルを
軽ーい気持ちで作ろうと思ったら、あまりいいサンプルがないのでメモ。

※ ツールについて

* 今回の記事をベースに、簡単なプロトタイプを作ってみた

https://dk521123.hatenablog.com/entry/2022/11/23/000000

目次

【1】今回やりたいこと
 1)値が入ってたら、動的に inputタグを生成する
 2)inputタグの動的生成のタイミングは、ユーザ入力後に行う
 3)値が入っていなかったら、動的生成を行わないよう制御
【2】サンプル

【1】今回やりたいこと

* 以下、今回やりたい仕様は、以下の通り。
~~~~~~
1)値が入ってたら、動的に inputタグを生成する
2)inputタグの動的生成のタイミングは、ユーザ入力後に行う
3)値が入っていなかったら、動的生成を行わないよう制御
~~~~~~

1)値が入ってたら、動的に inputタグを生成する

* createElement() で生成
 => これは全然難しくない
 => 以下の関連記事を参照のこと。

DOM ~ 基本編 / オブジェクト作成 ~
https://dk521123.hatenablog.com/entry/2022/11/19/000000

2)inputタグの動的生成のタイミングは、ユーザ入力後に行う

* 今回の場合、blurイベントで発火させる
 => ネットで調べると、ボタン押下してってのがよくあったが。
 => ここまで大したことない。
 => 以下の関連記事を参照のこと。

イベント ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2022/11/20/000000

3)値が入っていなかったら、動的生成を行わないよう制御

* 今回の場合、getElementsByTagName() で要素を取得してチェックする

DOM ~ 基本編 / オブジェクト取得 ~
https://dk521123.hatenablog.com/entry/2022/11/21/000000

【2】サンプル

<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<form id="mainForm">
  <div id="div0">
    <input id="itemName0" name="itemName0" class="inputText" type="text" maxlength="5" size="10">
  </div>
</form>
</body>
<script>
var index = 0;

function canAddInput() {
  var canAdd = true;
  console.log("Check input values");
  const inputTexts = document.getElementsByTagName("input");
  for(let i = 0 ; i < inputTexts.length ; i ++ ) {
    console.log(`Value = ${inputTexts[i].value}`);
    if (!inputTexts[i].value) {
      canAdd = false;
      break;
    }
  }
  return canAdd;
}

function addRow() {
  if (!canAddInput()) {
    console.log("No need to add inputs any more");
    return;
  }
  
  console.log("Need to add input!!");
  index = index + 1;
  const newDiv = document.createElement("div");
  newDiv.setAttribute("id", `div{index}`);
  
  const newInput = document.createElement("input");
  newInput.setAttribute("id", `itemName${index}`);
  newInput.setAttribute("name", `itemName${index}`);
  newInput.setAttribute("css", "inputText");
  newInput.setAttribute("type", "text");
  newInput.setAttribute("maxlength", "5");
  newInput.setAttribute("size", "10");
  newInput.setAttribute("value", "");
  newInput.addEventListener('blur', addRow);

  const mainForm = document.getElementById("mainForm");
  mainForm.appendChild(newDiv);
  newDiv.appendChild(newInput);
}

const inputText = document.getElementById("itemName0");
inputText.addEventListener('blur', addRow);
</script>
<html>

関連記事

DOM ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2011/01/07/012520
DOM ~ 基本編 / getElementById() ~
https://dk521123.hatenablog.com/entry/2011/12/29/200502
DOM ~ 基本編 / オブジェクト取得 ~
https://dk521123.hatenablog.com/entry/2022/11/21/000000
DOM ~ 基本編 / オブジェクト作成 ~
https://dk521123.hatenablog.com/entry/2022/11/19/000000
DOM ~ 基本編 / オブジェクト操作 ~
https://dk521123.hatenablog.com/entry/2011/01/09/155824
JavaScript覚書
https://dk521123.hatenablog.com/entry/2010/01/15/191626
イベント ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2022/11/20/000000
JS で テスト用の超簡易なCSVデータ生成ツールを作ってみる
https://dk521123.hatenablog.com/entry/2022/11/23/000000
JS で ファイル出力するCSVデータ生成ツールを作ってみる
https://dk521123.hatenablog.com/entry/2022/11/28/000000