水無瀬のプログラミング日記

プログラムの勉強した時のメモ

clasp + typescriptでAPIを作ってみる

はじめに

GoogleAppsScriptがほぼjsなのにes6でかけなかったり、
tsで書けなかったりちょっと残念だなとか思ってたら公式でサポートされてた。
今回はclaspを使ってtypescriptで書いてAPIを公開してみる。

TL;DR.

今回作ったサンプルコード

インストールする

$ npm install --save-dev @google/clasp
$ clasp -v 
>1.7.0

インストールはこれで完了。
globalにインストールしない場合はフォルダ構成に合わせてpathを通してください。
macならbash_profileに下記追記でOK。

export PATH=$PATH:./node_modules/.bin

プロジェクト作成

# ブラウザが開くと思うので任意のGoogleアカウントでログイン
$ clasp login 

# ソース置いとくフォルダ作成
$ mkdir src

# rootDirを"src"に指定して"sample"という名前のプロジェクト作成
$ clasp create --title "sample" --rootDir ./src

# どれか聞かれるから好みのものを選ぶ(今回はapi)
# APIはデフォルトだとエラーになるので下記URLから事前に「オンにしておく」
# https://script.google.com/home/usersettings
>? Clone which script?
>  standalone
>  docs
>  sheets
>  slides
>  forms
>  webapp
>❯ api

# サンプルのプログラムを作成
# claspではES6もTypeScriptも問題ない
$ touch src/sample.ts

これでプロジェクトの作成は問題ないはず。
サンプルプログラムは下記の通りにした。
(と言っても公式のサンプル丸コピだけど)

// Optional Types
let isDone: boolean = false;
let height: number = 6;
let bob: string = "bob";
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
enum Color {Red, Green, Blue};
let c: Color = Color.Green;
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data: string): void { // Void
 console.log(data);
}
showMessage('hello');

// Classes
class Hamburger {
  constructor() {
    // This is the constructor.
  }
  listToppings() {
    // This is a method.
  }
}

// Template strings
var name = 'Sam';
var age = 42;
console.log(`hello my name is ${name}, and I am ${age} years old`);

// Rest arguments
const add = (a, b) => a + b;
let args = [3, 5];
add(...args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`

// Spread operator (array)
let cde = ['c', 'd', 'e'];
let scale = ['a', 'b', ...cde, 'f', 'g'];  // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

// Spread operator (map)
let mapABC  = { a: 5, b: 6, c: 3};
let mapABCD = { ...mapABC, d: 7};  // { a: 5, b: 6, c: 3, d: 7 }

// Destructure map
let jane = { firstName: 'Jane', lastName: 'Doe'};
let john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' }
function sayName({firstName, lastName, middleName = 'N/A'}) {
  console.log(`Hello ${firstName} ${middleName} ${lastName}`)  
}
sayName(jane) // -> Hello Jane N/A Doe
sayName(john) // -> Helo John Smith Doe

// Export (The export keyword is ignored)
export const pi = 3.141592;

// Google Apps Script Services
var doc = DocumentApp.create('Hello, world!');
doc.getBody().appendParagraph('This document was created by Google Apps Script.');

// Decorators
function Override(label: string) {
  return function (target: any, key: string) {
    Object.defineProperty(target, key, { 
      configurable: false,
      get: () => label
    });
  }
}
class Test {
  @Override('test') // invokes Override, which returns the decorator
  name: string = 'pat';
}
let t = new Test();
console.log(t.name); // 'test'

pushしてみる

# rootDirに指定したフォルダの内容を全てpush
$ clasp push
└─ src/appsscript.json
└─ src/sample.ts
Pushed 2 files.

pushされたprojectを確認すると下記のようなgsに変化される。

var exports = exports || {};
var module = module || { exports: exports };
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

// 型定義
var isDone = false;
var height = 6;
var bob = "bob";
var list1 = [1, 2, 3];
var list2 = [1, 2, 3];
var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
var c = Color.Green;
var notSure = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data) {
    // Void
    Logger.log(data);
}
showMessage("hello");
// クラス
var Hamburger = /** @class */ (function () {
    function Hamburger() {
        // コンストラクタ
    }
    Hamburger.prototype.listToppings = function () {
        // メソッド
    };
    return Hamburger;
}());
// テンプレート文字列
var name = "Sam";
var age = 42;
console.log("hello my name is " + name + ", and I am " + age + " years old");
// Rest arguments
var add = function (a, b) { return a + b; };
var args = [3, 5];
add.apply(void 0, args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`
// スプレッド構文 (array)
var cde = ["c", "d", "e"];
var scale = ["a", "b"].concat(cde, ["f", "g"]); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
// スプレッド構文 (map)
var mapABC = { a: 5, b: 6, c: 3 };
var mapABCD = __assign({}, mapABC, { d: 7 }); // { a: 5, b: 6, c: 3, d: 7 }
// 分割代入
var jane = { firstName: "Jane", lastName: "Doe" };
var john = { firstName: "John", lastName: "Doe", middleName: "Smith" };
function sayName(_a) {
    var firstName = _a.firstName, lastName = _a.lastName, _b = _a.middleName, middleName = _b === void 0 ? "N/A" : _b;
    console.log("Hello " + firstName + " " + middleName + " " + lastName);
}
sayName(jane); // -> Hello Jane N/A Doe
sayName(john); // -> Helo John Smith Doe
// Export (The export keyword is ignored)
exports.pi = 3.141592;
// Google Apps Script の独自サービスの利用
var doc = DocumentApp.create("Hello, world!");
doc
    .getBody()
    .appendParagraph("This document was created by Google Apps Script.");
// デコレータ(高階関数)
function Override(label) {
    return function (target, key) {
        Object.defineProperty(target, key, {
            configurable: false,
            get: function () { return label; }
        });
    };
}
var Test = /** @class */ (function () {
    function Test() {
        this.name = "pat";
    }
    __decorate([
        Override("test") // invokes Override, which returns the decorator
    ], Test.prototype, "name");
    return Test;
}());
var t = new Test();
console.log(t.name); // 'test'

APIを作ってみる

とりあえず今回は外部から叩けることを確認する。
src/api.tsとしてファイルを作った。
中身は下記の通り。ただ単に適当なJSONを返した。

// doGetはGETで叩かれたときに実行される関数
function doGet() {
    const resData = JSON.stringify({ message: 'Hello World!' });
    // ContentServiceを利用して、responseを作成
    // そのまま返してもエラーになる
    ContentService.createTextOutput();
    const output = ContentService.createTextOutput();
    output.setMimeType(ContentService.MimeType.JSON);
    output.setContent(resData);
    // return response-data
    return output;
}

APIを使えるように設定する

「公開 > 実行可能APIとして導入」からAPIは初公開するとき警告出るけどここではスルーして続行。
スルーするとAPIIDが出てくるのでコピっておく。
スクリプトにアクセスできるユーザは「全員」にしておく。

次に「公開 > ウェブアプリケーションとして導入」から設定する。
プロジェクトバージョンは先程公開したAPIのバージョン、
次のユーザーとしてアプリケーションを実行は「自分」、
アプリケーションにアクセスできるユーザーは「全員(匿名ユーザを含む)」にして導入。
導入すると「現在のウェブ アプリケーションの URL」が出てくるからコピっておく。

APIを叩いてみる

ブラウザから直接叩くかcurl -L 【コピったURL】でOK。
{"message":"Hello World!"}みたいなレスポンスが帰ってくれば成功。

まとめ

今回はclasp + typescriptでGoogle Apps ScriptのAPIを作ってみた。
見た目はtsだったりするけど、中身はGASなのでちゃんとスプレッドシートとかGmailとかと連携はできるはず。
Googleのなんかを使うのはまたいずれ。
それでは今回はこの辺で。