見出し画像

[kintone] ブラウザからAPIで遊ぶ! アプリの魂を書き換えて他のアプリにコピー

こんにちは、キン担ラボの本橋です。

猛暑が来てますね

普段あんまり発生しない作業を楽にするTipsの紹介です。

『アプリの引っ越しに伴ってルックアップの参照先アプリを書き換える』なんて必要に迫られたときにこの記事のことを思い出していただけたらと思います。

アプリのコピーという作業は、アプリテンプレートが使えればともかく普通に手作業するとブラウザを2つ並べて同じようにフィールドを並べる、みたいなとんでもなく時間がかかる作業になりかねません。

そんなときに備えて、こんな手段があるよ、と覚えておくと役に立つことがあるかもしれません。

そんなの無い方が幸せですが。


今回は使うことができないginueの紹介

アプリ移行というと「あー、あれをつかうんだね」とご存知の方のために最初に紹介しておきますと、本来であればginueというツールを使います。

ginueを使うことができれば、kintoneアプリの中身をJSONで抜き出して、他のアプリの中に移し替えることができます。

[“優秀な管理職”ギニュー隊長はなぜ敗れた? 組織心理学の専門家に聞く「ドラゴンボールのチームワーク論」]| 【公式】ドラゴンボールオフィシャルサイト より

これがまーメチャクチャ便利で、僕も普段からアプリ管理に利用させていただいています。抜き出したJSONにはフィールドの設定やら配置情報やら、ルックアップの参照先などの情報もすべて含まれています。

本当にまるごと移し替えることができるのです。(APIで取り出せる範囲、という制限はありますが)

この移行作業の折には、別のアプリに移し替える前にJSONを手動で書き換えてあげることでルックアップや関連レコードの参照先も変更することができるんですね。こうなるとヘブンズドア感も出てきます。

が、今回は理由があってginueを使えなかったのでブラウザのデベロッパーツール上でkintone APIを叩きながら作業を進めます。

アプリを移設する手順

作業の全体像としては、

① 開発環境にある移行元のアプリAのIDを調べる
② 本番環境に移行先のアプリBを新しく作る
③ Aの魂をアレコレして(ヘブンズドア)
④ Bに移す(ボディチェンジ)

という手順です。

参照関係がなければ『アプリの所属するスペースを変更する』機能で済む話です。便利になりました。

ところが今回はマスタアプリや関連レコードの参照を書き換えなければなりません。

①移行元のアプリAの準備

ここではルックアップも関連レコードも入っている営業支援パックで実験してみます。適当な実験スペースを作成して、営業支援パックをインストールしてください。

本アプリパックは営業で顧客や案件、商談の管理をすることを目的に構成されたサンプルのアプリパックです。
以下の3つのアプリで構成されています。
・顧客管理(営業支援パック)
・案件管理(営業支援パック)
・活動履歴(営業支援パック)

このうち、活動履歴アプリをアプリAのとして扱います。アプリを開いてみると、こんなフィールド。

ルックアップも関連レコードも含みます

デベロッパーツールで魂を抜く

ここでブラウザからデベロッパーツールを開いて、このアプリのフィールド情報を取得してみましょう。

999のところはアプリIDです。コピー元としたいアプリのアプリIDを指定してあげてください。

kintone.api(kintone.api.url('/k/v1/app/form/fields.json',true),'GET', {'app': 999}).then(resp => console.log({resp}))

うまくいくとこんな具合にフィールド情報を取得できます。

ここで resp にカーソルを合わせて右クリックして、"Copy object"を選びます。これでクリップボードにフィールド情報がコピーされました。

コピーできたら、クリップボードのJSONをお使いのテキストエディタにペーストしておいてください。これをアプリAの魂と呼ぶことにします。

このコピーしたオブジェクトjsonをkintone APIにぽいっと投げてあげることで別のアプリにフィールド情報を移し替えることができます。

②移行先となる空のアプリを作る

つづいて、抜き出した魂を入れ込む移行先のアプリを作ります。なんのフィールドも持たないガワだけのアプリで構いません。

現在はまだAPIから新しくアプリを作ることができません。手作業で。

手順②はこれだけです。他の2つのアプリも移行する場合は必要な数だけ、ガワだけのアプリを作っておいてください。

③ Aの中身をアレコレして(ヘブンズドア)

抜き出した魂はそのままだと他のアプリに入りません。魂の形を整えて上げる必要があります。整えポイントは3つあります。

  1. 「作成日時」とかの標準搭載のフィールドについて設定しようとするとエラーになってしまうので削除

  2. revision も不要なので削除

  3. 移行先のアプリIDをappプロパティとして追加する

1も2も、APIに投げるまえにJSONから手作業で削除してあげましょう。3は移行先アプリのIDです。この移行先アプリIDにうっかり別のアプリを指定してしまうと大事故が起こります。慎重に。

1.標準搭載フィールドの削除

ちなみに1の標準搭載フィールドを削除しないまま投げるとこんなエラーが出ます。このエラーメッセージに登場するフィールドをjsonから削除します。

{
    "properties[作成日時].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[作成者].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[カテゴリー].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[更新日時].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[更新者].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[作業者].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[ステータス].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    },
    "properties[レコード番号].type": {
        "messages": [
            "組み込みのフィールドは指定できません。"
        ]
    }
}

削除が必要なフィールドはこのあたりです。

  • 作成日時

  • 作成者

  • カテゴリー

  • 更新日時

  • 更新者

  • 作業者

  • ステータス

  • レコード番号

手作業で一個一個除去してあげるとAPIの勉強にもつながると思います。
が、面倒な方は以下のスクリプトで一括処理してしまってください。

filtering = ["作成日時", "作成者", "カテゴリー", "更新日時", "更新者", "作業者", "ステータス", "レコード番号"]
params.properties = Object.assign(...Array.from(Object.keys(params.properties)).filter(key=>{ return !filtering.includes(key)}).map(k => { return {[k]: params.properties[k]} }))

1行目でfilteringという配列に除去したいフィールドのキーを設定して、2行目でフィールド情報を再構成しています。

ChatGPTによる解説を用意しました。詳細が気になる方はこちらのリンクをどうぞ。

 2.revisionの削除

アプリはバージョン番号を持っていて、履歴がサーバー側に保存されています。revisionを指定することで、どのバージョンに対して修正を行うかを指定できます。

このプロパティを消す

ここでは最新バージョンを対象としたいので、revisionプロパティを削除しておいてください。

3.移行先のアプリIDをappプロパティとして追加する

ちょっと長いですが以上の作業を行ったjsonを貼り付けておきます。

{
    "app": 999,
    "properties": {
        "顧客管理レコード番号_関連レコード一覧紐付け用": {
            "type": "NUMBER",
            "code": "顧客管理レコード番号_関連レコード一覧紐付け用",
            "label": "顧客管理レコード番号(関連レコード一覧紐付け用)",
            "noLabel": false,
            "required": false,
            "minValue": "",
            "maxValue": "",
            "digit": false,
            "unique": false,
            "defaultValue": "",
            "displayScale": "",
            "unit": "",
            "unitPosition": "BEFORE"
        },
        "対応者": {
            "type": "USER_SELECT",
            "code": "対応者",
            "label": "対応者",
            "noLabel": false,
            "required": false,
            "entities": [],
            "defaultValue": [
                {
                    "type": "FUNCTION",
                    "code": "LOGINUSER()"
                }
            ]
        },
        "案件管理レコード番号_関連レコード一覧紐付け用": {
            "type": "NUMBER",
            "code": "案件管理レコード番号_関連レコード一覧紐付け用",
            "label": "案件管理レコード番号(関連レコード一覧紐付け用)",
            "noLabel": false,
            "required": false,
            "minValue": "",
            "maxValue": "",
            "digit": false,
            "unique": false,
            "defaultValue": "",
            "displayScale": "",
            "unit": "",
            "unitPosition": "BEFORE"
        },
        "案件詳細": {
            "type": "REFERENCE_TABLE",
            "code": "案件詳細",
            "label": "案件詳細",
            "noLabel": false,
            "referenceTable": {
                "relatedApp": {
                    "app": "508",
                    "code": ""
                },
                "condition": {
                    "field": "案件管理レコード番号_関連レコード一覧紐付け用",
                    "relatedField": "レコード番号"
                },
                "filterCond": "",
                "displayFields": [
                    "確度",
                    "受注予定日",
                    "提案プラン",
                    "オプション",
                    "合計費用"
                ],
                "sort": "レコード番号 desc",
                "size": "5"
            }
        },
        "部署名": {
            "type": "SINGLE_LINE_TEXT",
            "code": "部署名",
            "label": "部署名",
            "noLabel": false,
            "required": false,
            "minLength": "",
            "maxLength": "",
            "expression": "",
            "hideExpression": false,
            "unique": false,
            "defaultValue": ""
        },
        "担当者名": {
            "type": "SINGLE_LINE_TEXT",
            "code": "担当者名",
            "label": "担当者名",
            "noLabel": false,
            "required": false,
            "minLength": "",
            "maxLength": "",
            "expression": "",
            "hideExpression": false,
            "unique": false,
            "defaultValue": ""
        },
        "案件名": {
            "type": "SINGLE_LINE_TEXT",
            "code": "案件名",
            "label": "案件名",
            "noLabel": false,
            "required": false,
            "lookup": {
                "relatedApp": {
                    "app": "508",
                    "code": ""
                },
                "relatedKeyField": "案件名",
                "fieldMappings": [
                    {
                        "field": "案件管理レコード番号_関連レコード一覧紐付け用",
                        "relatedField": "レコード番号"
                    }
                ],
                "lookupPickerFields": [
                    "作成日時",
                    "顧客名",
                    "案件名"
                ],
                "filterCond": "",
                "sort": "レコード番号 desc"
            }
        },
        "商談メモ": {
            "type": "MULTI_LINE_TEXT",
            "code": "商談メモ",
            "label": "商談メモ",
            "noLabel": false,
            "required": false,
            "defaultValue": ""
        },
        "対応内容": {
            "type": "DROP_DOWN",
            "code": "対応内容",
            "label": "対応内容",
            "noLabel": false,
            "required": false,
            "options": {
                "見積提示": {
                    "label": "見積提示",
                    "index": "5"
                },
                "セミナー": {
                    "label": "セミナー",
                    "index": "4"
                },
                "ご挨拶": {
                    "label": "ご挨拶",
                    "index": "1"
                },
                "問い合わせ": {
                    "label": "問い合わせ",
                    "index": "0"
                },
                "注文書回収": {
                    "label": "注文書回収",
                    "index": "6"
                },
                "商談": {
                    "label": "商談",
                    "index": "2"
                },
                "フェア": {
                    "label": "フェア",
                    "index": "3"
                },
                "その他": {
                    "label": "その他",
                    "index": "7"
                }
            },
            "defaultValue": ""
        },
        "顧客名": {
            "type": "SINGLE_LINE_TEXT",
            "code": "顧客名",
            "label": "顧客名",
            "noLabel": false,
            "required": false,
            "lookup": {
                "relatedApp": {
                    "app": "509",
                    "code": ""
                },
                "relatedKeyField": "顧客名",
                "fieldMappings": [
                    {
                        "field": "部署名",
                        "relatedField": "部署名"
                    },
                    {
                        "field": "担当者名",
                        "relatedField": "担当者名"
                    },
                    {
                        "field": "顧客管理レコード番号_関連レコード一覧紐付け用",
                        "relatedField": "レコード番号"
                    }
                ],
                "lookupPickerFields": [
                    "顧客名",
                    "部署名",
                    "担当者名"
                ],
                "filterCond": "",
                "sort": "レコード番号 desc"
            }
        },
        "添付ファイル": {
            "type": "FILE",
            "code": "添付ファイル",
            "label": "添付ファイル",
            "noLabel": false,
            "required": false,
            "thumbnailSize": "150"
        },
        "対応日時": {
            "type": "DATE",
            "code": "対応日時",
            "label": "対応日時",
            "noLabel": false,
            "required": false,
            "unique": false,
            "defaultValue": "",
            "defaultNowValue": true
        }
    }
}

このときルックアップ("lookup"属性)や関連レコード("type": "REFERENCE_TABLE")の要素を探して、対応するアプリIDを書き換えておくとルックアップや関連レコードの参照先アプリを差し替えることができます。ヘブンズドアです。

④ Bに移す(ボディチェンジ)

ようやくギニュー隊長の出番。新しいアプリにアプリAの魂(フィールド情報)を入れ込んでいきます。新しいアプリの画面に移動して、デベロッパーツールを開きます。

params = (さきほどのjsonをコピペ)
こんな表示が出れば成功

APIを呼び出していきます。

kintone.api(kintone.api.url('/k/v1/preview/app/form/fields.json',true),'POST', params).then(resp => console.log({resp}))

特にエラーが出なければ、アプリBのカスタマイズ画面を開くとフィールドが追加されているはずです。

同じようにレイアウト情報もコピーしてあげましょう。ここでは不要フィールドの削除操作は行わなくても大丈夫です。revesionの削除と、appの追記だけしてあげてください。

layouts = (ここにコピペ)

kintone.api(kintone.api.url('/k/v1/preview/app/form/layout.json',true),'PUT', layouts).then(resp => console.log({resp}))

エラーが出なければレイアウトも反映されているはずです。アプリカスタマイズ画面を開いてみてください。

フォームとレイアウトのコピーができた!

同じように、一覧やグラフの設定もAPI経由でコピーしてあげることができます。

他の情報も手ginueしてみたかったり、エラーが出てうまくコピーできないという方は、表示されたエラーメッセージの中身と公式ドキュメントを見比べながら調べてみてください。

調べる過程できっとkintoneと仲良くなれると思います。

余談: そもそもの経緯

先日、kintoneのアプリのフィールド情報一式をまとめて他の環境のアプリに移し替えるという作業を行いました。

もともとは参加プロジェクトのミーティングで一人のメンバーが

『開発用に作ったkintoneアプリを本番スペースに移行する』

という作業を予定していたんですね。

ところがそこにはマスタアプリを参照するルックアップが含まれていて、参照先であるマスタアプリ自体も差し替えが必要とのことでした。手作業で行う想定でいたようです。なんだかソワソワしてくる話です。

「それは手作業だとルックアップを一つ一つ作り変えるような作業になって膨大な時間がかかってしまいます」

と僕から伝えて、その作業は僕の方で引き受けることにしました。

ルックアップの差し替えなどがなければ、これはアプリテンプレートを使ったり、スペーステンプレートを作ったりすれば簡単な作業かと思います。

ただこのときは、ルックアップや関連レコードの差し替えがありました。加えて解せぬことにルックアップ先のアプリ管理権限をもらえていなかったり(※注)、kintoneがセキュアアクセス環境にあったりして一筋縄では行かない状況でした。

※注:アプリテンプレート作成については、関連レコードやルックアップで参照している先のアプリ管理権限が必要です。

そんなこんなで『手作業で一連のアプリを移設する』という作業を行った記録とともに、その手順を紹介してみました。

なにかのときにお役に立てれば幸いです。

この記事が気に入ったらサポートをしてみませんか?