話題の記事

Admin.jsを使って面倒な管理画面をサクッと作ろう

ExpressとPrismaで動くサーバーにAdmin.jsを導入して処理をカスタマイズしてみました
2023.09.15

こんにちは、CX事業本部Delivery部サーバーサイドチームのmorimorkochanです。

突然ですが「あぁ〜管理画面作るのめんどくせ〜」って思うことはないですか?
例えばRDBと接続されたRESTfulなAPIサーバーを作っていて、一部の管理者向けに管理画面を作りたいが管理画面にこだわりがない場合などなど。

そんな時に便利なのが、Admin.jsです。Admin.jsは管理画面を簡単に作成できるフレームワークです。オープンソースとして公開されており、クラウドにデプロイされているサービスを利用する場合は月額料金がかかりますが手動でサーバーに組み込んでデプロイする場合は無料です。 Admin.jsを使うと、RDBで管理される各テーブルごとにCRUD画面を簡単に作成することができます。これによってRDBと同じプロパティを何度も定義したり同じようなCRUDコードを何度も記述する必要はありません。

今回は、Prismaが利用されているExpress製サーバーに対してAdmin.jsを導入し、管理画面を作り、自分で操作をカスタマイズしてみたので紹介します。

環境

  • Node.js v18.16.1
  • Admin.js v7.2.1
  • Express v4.18.2
  • Prisma v5.3.0

導入対象のExpress製サーバー

Admin.jsを導入する対象となるサーバーは、prisma-examplesに用意されているrest-expressを使ってみます。
以下の手順でDB(sqlite3)を用意しサーバーを立ち上げます

npx try-prisma@latest --template typescript/rest-express
cd typescript_rest-express
# DBの準備
npx prisma migrate dev
# Expressサーバー起動
# Admin.jsはCommonJSでは動作しないので`--esm`
npm run dev --esm

この状態で`http://localhost:3000/feed`を叩くと以下のようなデータが返却されると思います

[{"id":1,"createdAt":"2023-09-14T10:27:27.601Z","updatedAt":"2023-09-14T10:27:27.601Z","title":"Join the Prisma Slack","content":"https://slack.prisma.io","published":true,"viewCount":0,"authorId":1,"author":{"id":1,"email":"alice@prisma.io","name":"Alice"}},{"id":2,"createdAt":"2023-09-14T10:27:27.605Z","updatedAt":"2023-09-14T10:27:27.605Z","title":"Follow Prisma on Twitter","content":"https://www.twitter.com/prisma","published":true,"viewCount":0,"authorId":2,"author":{"id":2,"email":"nilu@prisma.io","name":"Nilu"}},{"id":3,"createdAt":"2023-09-14T10:27:27.606Z","updatedAt":"2023-09-14T10:27:27.606Z","title":"Ask a question about Prisma on GitHub","content":"https://www.github.com/prisma/prisma/discussions","published":true,"viewCount":0,"authorId":3,"author":{"id":3,"email":"mahmoud@prisma.io","name":"Mahmoud"}}]

このDBにはUserテーブルとPostテーブルの2つが用意されており、データもすでに入っている状態です。

Admin.jsを導入する

Admin.jsでは、さまざまなタイプのサーバーに対して管理画面を提供できるように、プラグインという各サーバーに対応したモジュールが提供されています。今回はExpress製サーバーに対応したプラグインを利用します。

導入方法が公式で提供されていて、それに従うだけでうまくいったのでここは省略します。

https://docs.adminjs.co/installation/plugins/express

また、Admin.jsはさまざまなタイプのデータベースに対応していて、アダプターという対応したモジュールが提供されています。今回はPrismaを利用しているので、Prisma用のアダプターを利用します。

導入方法は公式で提供されてます。

https://docs.adminjs.co/installation/adapters/prisma

ですが、Prismaのv4.14.0以降、PrismaClientから_basedmmfが削除されているため上記手順では動きません

なので代わりにAdminJSPrisma.getModelByName()を利用する必要があります。どうやら公式ドキュメントの更新が追いついていないようです

  const adminOptions = {
    resources: [{
-      resource: { model: dmmf.modelMap.Publisher, client: prisma },
+      resource: { model: AdminJSPrisma.getModelByName('Publisher'), client: prisma },
      options: {},
    }],
  }

また、テーブルごとにresouceの定義は必要なため、PostテーブルとUserテーブルで有効にする場合は以下のようになります。

  const adminOptions = {
    resources: [
      {resource: { model: AdminJSPrisma.getModelByName('Post'), client: prisma },options: {}},
      {resource: { model: AdminJSPrisma.getModelByName('User'), client: prisma },options: {}}
    ],
  }

ここで`http://localhost:3000/admin`を開いてみると、サイドメニューにresouceで指定された各テーブルが表示されており、一覧表示・詳細表示・登録・編集・削除のいずれもできるようになっています。

adminjs一覧画面

adminjs編集画面

上記画像のように、一覧表示画面で検索ができるようになっていたり、編集画面で外部キーのカラムは自動で対象のテーブルのレコードをプルダウンで表示してくれたりと細かいところまで使いやすくなっています、超有能です。 しかもSPAで動作しているので、サクサク動きます。

認証機能を入れる

このままでは/adminを開くと誰でも管理画面を利用できてしまいますが、Admin.jsでは簡単に認証機能を入れることが可能です。

const adminRouter = AdminJSExpress.buildAuthenticatedRouter(admin, {
  cookiePassword: "awesomeCookiePassword",
  authenticate: async (email, password) => {
    // ここでemailとpasswordで認証する処理を書く
    if (email === "superuser@example.com" && password === "awesomePassword") {
        // 認証成功時はユーザー情報を返す
        return {
            email,
            password
        }    
    }
    // 認証失敗時はnullを返す
    return null
  }
})

再起動してアクセスするとこんな感じの認証画面にリダイレクトされます。思ったよりちゃんとしてますね

adminjs login画面

上記のコードを見てわかる通り、認証情報はサーバーのセッションで管理されており、メモリに保存されています。 なので、サーバーが再起動される環境や複数台のサーバーでロードバランシングしている環境には不向きです(後者はスティッキーセッションを利用して解消することが可能ですが)

操作をカスタマイズする

Admin.jsではデータの操作として登録/編集/削除が用意されていますが、オリジナルの操作を追加することも簡単に可能です。 例えば、Postテーブルの特定の単一レコードに対して記事を公開するpublishという操作を行いたい場合は、アクションを新たに定義します

アクションは以下のようにresourceactionsにプロパティを追加することで可能です。

const publishActionHandler: ActionHandler<RecordActionResponse> = async (request, response, context) => {
  const post = context.record!
  post.set('published', true)
  await post.save(context)
  return {
    notice: {
      message: "公開されました🚀"
    },
    record: post.toJSON(context.currentAdmin)
  }
}
const postResource = {
  resource: { model: AdminJSPrisma.getModelByName('Post'), client: prisma },
  options: {
    actions: {
      publish: {
        actionType: 'record',
        component: false,
        handler: publishActionHandler,
      },
    },
  },
}
const admin = new AdminJS({
  rootPath: '/admin',
  resources: [
      postResource,
      {resource: { model: AdminJSPrisma.getModelByName('User'), client: prisma },options: {},}
  ],
})

操作対象のレコードはcontextrecordプロパティで取得できるのですが型安全ではないため、実際に利用する際は追加で型注釈をしてあげると良いと思います。また、contextにはcurrentAdminというプロパティがあり、現在ログインしているユーザーの情報が格納されています。

実際に操作するとこんな感じです。操作完了後にnotice.messageに指定したメッセージが表示されていてなおかつpublishedYesになっていることがわかります。

adminjs-操作デモ

また、他にもさまざまなオプションが用意されています。

  • actionTyperecordからbulkにすることで複数レコードに対して操作する(例:複数レコードを削除する)
  • isAccessibleを使って誰が操作できるかを制御する
  • componentを使ってUIをカスタマイズする

感想

  • 今回紹介した以外にもいろいろな機能が提供されています。
    • ファイルをAWS S3やGoogle Cloud Storageにアップロードする機能
    • 一括でデータのインポートエクスポートができる機能
    • 管理画面の各種操作を全て記録する機能
  • 特にReactでUIをカスタマイズできる点は本当に優れてると思います。実際の開発では、徐々にUIがカスタマイズされていってテンプレートエンジンでは複雑性が高くなり開発できなくなる、みたいなことがなさそうに感じます
  • 日本語には未対応です。i18nの設定は存在しますがsrc/locale日本語の設定はありません
  • Python/DjangoのDjangoAdminがとても使いやすく爆速で開発できた印象があったので、Node.jsでも同等のものがないかを今回は探してみました
    • サードパーティーのライブラリの数や種類はDjangoAdminには及びませんが、基本的なCRUD操作はもちろんのこと、欲しい機能があらかた揃っているので、とても満足しています。
  • 強いて不満点を言うなら
    • 処理をカスタマイズする際に型安全ではないところもあるのでそこは要注意です。
    • ドキュメントが古いところがあったり不足しているところがあるので、自分でガリガリ型定義を読む必要があります。
    • それでも動的型付け言語の場合と比べると、型定義を読むことでどういう処理をすれば良いかがわかるので、そこまで苦ではありませんでした。
  • DMMFという概念を初めて知ったのですが、Data Model Meta Formatというモデルのメタ情報のことを指しているようです。
    • https://github.com/prisma/prisma/blob/main/ARCHITECTURE.md#the-dmmf-or-data-model-meta-format
    • ググった感じだとDomain Modeling Made Functionalを略してもこう呼ばれるみたいです

是非みなさんもAdmin.jsを一度使ってみてください。