Gemini Canvas でも大体の GAS はかけるよーという話
JMUG #33 Notionで夏休みの宿題もJNUCもというイベントで 新しくできたApple Business ManagerのAPIを使ってNotion DBに端末一覧を作ってみた
というタイトルで登壇したのですが、その際に下記のようなGASのツールを作りました。
Cursor等で作ってももちろんいいのですが、本記事では検証も兼ねて Gemini Canvas をつかってこんな風に作ってみたというのを簡単に解説します。
Gemini Canvas をつかってコードを書く
基本的な流れとしては
Canvas で生成したコードを スクリプトエディタの画面に貼り付けていく
を繰り返していきます。
手動でスクリプトエディタの画面のコードを手動で直した方が早そうな時は
直したスクリプトエディタの画面のコードを Canvas 側に貼り直します。
これらを繰り返すことで完成に近づけていきます。
全部のやり取りを書いているわけじゃないのですがこんな感じで書いているよーというのが分かれば幸いです。
ベースのGASを作る
下記のようなプロンプトで情報を与えつつベースのコードを書いてもらいます。
ポイントは少しずつ完成形に近づけることです。最初からすべてを実現できるプロンプトにする必要はありません。
そのため、アクセストークンの取得等の処理は後で書いてもらうで良い、と指示を出しています。
Apple Business Manager の API(https://developer.apple.com/documentation/applebusinessmanagerapi)
を使ってデバイス情報をスプレッドシートに書き込むGoogle Apps Script を書いてください
デバイス情報を取得するAPI(https://developer.apple.com/documentation/applebusinessmanagerapi/get-org-devices)があります。
アクセストークンは取得済みとして仮定してコードを書いてください
※ 画像のコードは完成系なのでこの時点で出てくるコードではありません
コンテナバインドスクリプトとして作りたいので直してもらう
GASファイル単体で動くのをスタンドアロンスクリプトといい、スプレッドシートに紐づくものをコンテナバインドスクリプトといいます。
最初の私の指示だしにそこが記載されてなかったので指示を出して直してもらいます
コンテナバインドスクリプトなので開いているスプレッドシートを書き換える形にしてください
スクリプトプロパティに移す
GASのコードを自動生成するとスクリプトプロパティを使わずにコードにベタ書きでトークン等を書こうとします。
そのため、スクリプトプロパティに移してーというのを指示出しします。
ACCESS_TOKEN はスクリプトプロパティから取得するようにしてもらえますか
実行してみたらAPIのリクエストがエラーになったので手動で指示出しします
細かいところでAIが正しく設定できないこともあります、コードを手動でURLを書き換えてもいいのですが今回は依頼して直してもらいました。
デバイス取得のURLは
https://api-business.apple.com/v1/orgDevices
です
誤字とかは気にせずに進めて大丈夫です
AIは賢いので多少のこの手の誤字があっても解釈してくれます、きれいな文章を作ろうとせずに依頼していきましょう。
https://api-business.apple.com/v1/orgDevicesを余分時にlimit =1000を入れてください
アクセストークンがベタ書きになっているので取得する処理を書いてもらう
Curlコマンドが公式ドキュメントのサンプルにあったので、それを渡してGASで書いてもらいました。
アクセストークンを取得する処理を書いてください
Curlコマンドの例
Notionに登録する処理を書いてもらう
これぐらい適当なプロンプトでも解釈して書いてくれました。
Notion の特定のDBにこのスプレッドシートの行を1ページとして書き込む処理
完成
参考前に完成したコードを貼っておききます。
本記事で書いてないやり取りが5つぐらいあり、手動でコードを直したのも何回かあった、という感じです。
/**
* @fileoverview Fetches device information from Apple Business Manager API,
* writes it to a Google Spreadsheet, and syncs it to a Notion database.
* @version 3.0
*/
// --- 事前設定 ---
// 1. Apps Scriptエディタの左メニューから「プロジェクトの設定」(歯車アイコン)をクリックします。
// 2. 「スクリプト プロパティ」セクションで以下のプロパティを設定します。
// - ABM_CLIENT_ID: (例: BUSINESSAPI.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
// - ABM_CLIENT_ASSERTION: API認証用のJWT文字列
// - NOTION_API_KEY: Notionの内部インテグレーションシークレット
// - NOTION_DATABASE_ID: 同期対象のNotionデータベースID
// --- スクリプト本体 ---
/**
* スプレッドシートを開いたときにカスタムメニューを追加します。
*/
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('ABM・Notion連携')
.addItem('1. ABMアクセストークンを更新', 'updateAccessToken')
.addSeparator()
.addItem('2. デバイスリストを取得', 'fetchAndWriteAppleDevices')
.addSeparator()
.addItem('3. Notion DBへ同期', 'syncToNotion')
.addToUi();
}
/**
* スクリプトプロパティのJWTを使い、Appleからアクセストークンを取得して保存します。
*/
function updateAccessToken() {
const ui = SpreadsheetApp.getUi();
try {
const props = PropertiesService.getScriptProperties();
const clientId = props.getProperty('ABM_CLIENT_ID');
const clientAssertion = props.getProperty('ABM_CLIENT_ASSERTION');
if (!clientId || !clientAssertion) {
throw new Error('スクリプトプロパティに `ABM_CLIENT_ID` または `ABM_CLIENT_ASSERTION` が設定されていません。');
}
const tokenUrl = 'https://account.apple.com/auth/oauth2/token';
const payload = {
'grant_type': 'client_credentials',
'client_id': clientId,
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': clientAssertion,
'scope': 'business.api'
};
const options = {
'method': 'post',
'contentType': 'application/x-www-form-urlencoded',
'payload': payload,
'muteHttpExceptions': true
};
Logger.log(`アクセストークンをリクエストします: ${tokenUrl}`);
const response = UrlFetchApp.fetch(tokenUrl, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode !== 200) {
throw new Error(`トークンの取得に失敗しました (Code: ${responseCode}): ${responseBody}`);
}
const result = JSON.parse(responseBody);
const accessToken = result.access_token;
if (!accessToken) {
throw new Error('レスポンスにアクセストークンが含まれていません。');
}
// 取得したアクセストークンをスクリプトプロパティに保存
props.setProperty('ABM_ACCESS_TOKEN', accessToken);
ui.alert('成功', '新しいアクセストークンを取得し、保存しました。', ui.ButtonSet.OK);
} catch (e) {
Logger.log(e);
ui.alert('エラー', `アクセストークンの取得中にエラーが発生しました。\n\n${e.message}`, ui.ButtonSet.OK);
}
}
/**
* メイン関数:APIからデバイス情報を取得し、現在開いているスプレッドシートに書き込みます。
*/
function fetchAndWriteAppleDevices() {
// スクリプトプロパティからアクセストークンを取得
const ABM_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('ABM_ACCESS_TOKEN');
const ui = SpreadsheetApp.getUi();
// アクセストークンが設定されているか確認
if (!ABM_ACCESS_TOKEN) {
ui.alert('スクリプトプロパティに `ABM_ACCESS_TOKEN` が設定されていません。\n\n左メニューの「プロジェクトの設定」(歯車アイコン)からアクセストークンを設定してください。');
return;
}
// 現在開いているスプレッドシートを取得
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheetName = 'デバイスリスト';
let sheet = spreadsheet.getSheetByName(sheetName);
// 'デバイスリスト'という名前のシートがなければ作成、あればクリア
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName);
} else {
sheet.clear();
}
// 処理対象のシートをアクティブにする
spreadsheet.setActiveSheet(sheet);
Logger.log(`アクティブなスプレッドシートの「${sheetName}」シートに書き込みを開始します。`);
try {
// ヘッダー行を定義して書き込み
const headers = [
'id', 'デバイスモデル', 'シリアル番号', 'ソース', '注文番号', 'パート番号', 'ストレージ', 'MEID', 'IMEI', 'CSN/EID', 'status', 'color', '追加日'
];
sheet.appendRow(headers);
sheet.getRange('A1:M1').setFontWeight('bold'); // ヘッダーを太字に
let cursor = null;
let hasMore = true;
let totalDevices = 0;
// ページネーション対応:cursorがある限りループして全デバイスを取得
while (hasMore) {
const response = callDeviceApi(cursor, ABM_ACCESS_TOKEN);
const devices = response.data;
if (devices && devices.length > 0) {
// 取得したデバイス情報をスプレッドシートに書き込む
const rows = devices.map(device => {
const attrs = device.attributes;
return [
maskLastFourChars(device.id|| ''),
attrs.deviceModel || '',
maskLastFourChars(attrs.serialNumber || ''),
attrs.purchaseSourceType || '',
maskLastFourChars(attrs.orderNumber || ''),
attrs.partNumber || '',
attrs.deviceCapacity || '',
(attrs.meid || []).map(val => maskLastFourChars(val)).join('\n'),
(attrs.imei || []).map(val => maskLastFourChars(val)).join('\n'),
maskLastFourChars(attrs.eid || ''),
attrs.status || '',
attrs.color || '',
attrs.orderDateTime ? new Date(attrs.orderDateTime) : ''
];
});
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, headers.length).setValues(rows);
totalDevices += devices.length;
Logger.log(`${devices.length}件のデバイス情報を書き込みました。合計: ${totalDevices}件`);
}
// 次のページのカーソルを設定
console.log(response.meta)
if (response.meta.paging.nextCursor) {
cursor = response.meta.paging.nextCursor;
Logger.log(`次のページを読み込みます... (cursor: ${cursor})`);
} else {
hasMore = false;
}
}
// 列幅を自動調整
for (let i = 1; i <= headers.length; i++) {
sheet.autoResizeColumn(i);
}
Logger.log('すべてのデバイス情報の書き込みが完了しました。');
ui.alert(`処理が完了しました。合計 ${totalDevices} 件のデバイス情報を「${sheetName}」シートに書き込みました。`);
} catch (e) {
Logger.log(`エラーが発生しました: ${e.toString()}`);
ui.alert(`処理中にエラーが発生しました。詳細はログを確認してください。\n\nエラー内容: ${e.message}`);
}
}
/**
* Apple Business Manager APIを呼び出してデバイス情報を取得します。
* @param {?string} cursor - 次のページを取得するためのカーソル。初回はnull。
* @param {string} accessToken - API認証用のアクセストークン。
* @returns {Object} APIからのレスポンスをパースしたJSONオブジェクト。
*/
function callDeviceApi(cursor, accessToken) {
const API_BASE_URL = 'https://api-business.apple.com/v1/orgDevices';
let url = `${API_BASE_URL}?limit=500`;
// cursorがあれば、クエリパラメータとして追加
if (cursor) {
url += `&cursor=${encodeURIComponent(cursor)}`;
}
const options = {
'method': 'get',
'headers': {
'Authorization': `Bearer ${accessToken}`,
// 'Content-Type': 'application/json'
},
'muteHttpExceptions': true // HTTPエラー時も例外をスローせず、レスポンスを返す
};
console.log(url, options);
Logger.log(`APIリクエストを送信します: ${url}`);
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 200) {
console.log(responseBody);
return JSON.parse(responseBody);
} else {
Logger.log(`APIエラー: Status Code ${responseCode}, Body: ${responseBody}`);
throw new Error(`APIリクエストに失敗しました。ステータスコード: ${responseCode}。詳細はログを確認してください。`);
}
}
/**
* スプレッドシートのデータをNotionデータベースに同期します。
*/
function syncToNotion() {
const ui = SpreadsheetApp.getUi();
try {
const props = PropertiesService.getScriptProperties();
const notionKey = props.getProperty('NOTION_API_KEY');
const databaseId = props.getProperty('NOTION_DATABASE_ID');
if (!notionKey || !databaseId) {
throw new Error('スクリプトプロパティに `NOTION_API_KEY` または `NOTION_DATABASE_ID` が設定されていません。');
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('デバイスリスト');
if (!sheet) {
throw new Error('シート「デバイスリスト」が見つかりません。');
}
const data = sheet.getDataRange().getValues();
const headers = data.shift(); // ヘッダー行を取得
// ↓↓↓ ご自身のNotionデータベースの構成に合わせて、このマッピングを編集してください ↓↓↓
const propertyMapping = {
// "Notionのプロパティ名": { type: "Notionのプロパティタイプ", column: "スプレッドシートの列名" },
"id": { type: "title", column: "id" },
"シリアル番号": { type: "rich_text", column: "シリアル番号" },
"デバイスモデル": { type: "rich_text", column: "デバイスモデル" },
"ソース": { type: "rich_text", column: "ソース" },
"注文番号": { type: "rich_text", column: "注文番号" },
"パート番号": { type: "rich_text", column: "パート番号" },
"ストレージ": { type: "rich_text", column: "ストレージ" },
"MEID": { type: "rich_text", column: "MEID" },
"IMEI": { type: "rich_text", column: "IMEI" },
"CSN/EID": { type: "rich_text", column: "CSN/EID" },
"status": { type: "rich_text", column: "status" },
"color": { type: "rich_text", column: "color" },
"追加日": { type: "date", column: "追加日" },
};
// ↑↑↑ ここまで ↑↑↑
const headerMap = headers.reduce((obj, header, i) => {
obj[header] = i;
return obj;
}, {});
ui.alert('Notionへ同期開始', `合計 ${data.length} 件のデータを同期します。完了までお待ちください。`, ui.ButtonSet.OK);
let successCount = 0;
let errorCount = 0;
data.forEach((row, index) => {
try {
const notionProperties = {};
for (const propName in propertyMapping) {
const mapping = propertyMapping[propName];
const colIndex = headerMap[mapping.column];
const cellValue = row[colIndex];
if (cellValue === undefined || cellValue === '') continue;
// Notion APIのペイロードを作成
switch (mapping.type) {
case 'title':
notionProperties[propName] = { title: [{ text: { content: cellValue.toString() } }] };
break;
case 'rich_text':
notionProperties[propName] = { rich_text: [{ text: { content: cellValue.toString() } }] };
break;
case 'date':
notionProperties[propName] = { date: { start: new Date(cellValue).toISOString() } };
break;
}
}
// デバイスモデルからアイコンを決定
const deviceModel = row[headerMap['デバイスモデル']];
const icon = getDeviceIcon(deviceModel);
callNotionCreatePageApi(notionKey, databaseId, notionProperties, icon);
successCount++;
Utilities.sleep(350);
} catch (e) {
errorCount++;
Logger.log(`行 ${index + 2} の同期に失敗しました: ${e.message}`);
}
});
ui.alert('同期完了', `同期が完了しました。\n成功: ${successCount}件\n失敗: ${errorCount}件`, ui.ButtonSet.OK);
} catch (e) {
Logger.log(e);
ui.alert('エラー', `Notionへの同期中にエラーが発生しました。\n\n${e.message}`, ui.ButtonSet.OK);
}
}
/**
* Notion APIを呼び出してページを作成します。
* @param {string} apiKey - Notion APIキー
* @param {string} dbId - Notion データベースID
* @param {Object} properties - ページのプロパティ
* @param {?Object} icon - ページのアイコンオブジェクト (例: { type: "emoji", emoji: "📱" })
*/
function callNotionCreatePageApi(apiKey, dbId, properties, icon) {
const url = 'https://api.notion.com/v1/pages';
const payload = {
parent: { database_id: dbId },
properties: properties
};
if (icon) {
payload.icon = icon;
}
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Notion-Version': '2022-06-28'
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode !== 200) {
throw new Error(`Notion APIリクエストに失敗しました (Code: ${responseCode}): ${responseBody}`);
}
}
/**
* 文字列の末尾4文字をアスタリスクでマスキングします。
* @param {string} value - マスキング対象の文字列
* @returns {string} マスキング後の文字列
*/
function maskLastFourChars(value) {
if (typeof value !== 'string' || value.length <= 4) {
return value;
}
return value.slice(0, -4) + '****';
}
/**
* デバイスモデル名から適切なNotionアイコンオブジェクトを返します。
* @param {string} deviceModel - デバイスモデル名
* @returns {?Object} Notionアイコンオブジェクト、またはnull
*/
function getDeviceIcon(deviceModel) {
if (!deviceModel) return null;
const model = deviceModel.toLowerCase();
if (model.includes('iphone') || model.includes('ipad') || model.includes('ipod')) {
return { type: 'emoji', emoji: '📱' };
}
if (model.includes('mac')) { // MacBook, iMac, Mac mini, etc.
return { type: 'emoji', emoji: '💻' };
}
if (model.includes('apple tv')) {
return { type: 'emoji', emoji: '📺' };
}
return null; // 上記以外はアイコンなし
}
おわりに
会話ベースで指示を出すにはコードを書ける力はもちろん一定必要なのですが、コードを書くという敷居はだいぶ下がったなぁと思います。
Cursor等のエディタを使わなくてもブラウザ上の Gemini Canvas だけで十分書けます。
筆者はGASを10年近く書いていますが、過去作ってきたものの9割ぐらいのGASアプリケーションはGemini Canvas だけで書けると思います。
GASは難しそうだからで書いたこなかった方もぜひチャレンジしてみるといいんじゃないかなぁと思います。
Discussion