Newtでなんちゃってマルチテナンシー的なことをしてみる

komosyu
Newt

目次

  1. はじめに
  2. API を取得してまとめる
  3. 参考
  4. さいごに

はじめに

今回は複数店舗それぞれで作成したコンテンツをまとめて扱うマルチテナンシー的なことがしたくて、でも CMS を自作するなんて大変そうだなということで、既存の HeadlessCMS でどうにかならないか?と思って、これまで使ってきた HeadlessCMS の中で使い勝手の良かった Newt でそういったことができるのか試してみました!

公式ドキュメントでも特にマルチテナンシーに関しての内容は見かけなかったし、利用規約的にも認められている内容かもわからないので実際に使う際には確認してみるといいかもしれないですね。

あと、今回の実装は実際には全然マルチテナンシーではないです w
正しいマルチテナンシーはこういうことらしいので。

複数のユーザーが同じサーバーやアプリケーション、データベースといったシステムやサービスを共有して利用する方式です。同一のサーバーやデータベースを仮想的に分割し、各ユーザーはそれぞれ与えられた領域を利用できるようになっています。

今回紹介する方法はそれぞれの店舗ごとにスペースを独立して持っている形になるので、上記にあるように共有して利用するという概念とは少し違いそうです。
これはマルチテナンシーではなく別の名称があるのでしょうか…

API を取得してまとめる

Newt のスペース作成方法や設定方法は以前のブログでも紹介しているのでよろしければ見てみてください。

Newt の管理画面側の設定はできたとして、ここからは実際に Next.js の API Routes を使って Newt のコンテンツを取得してみます。
ここでは Newt の中で作成したpostsという各店舗のモデルを 1 つの API にまとめていきます。

今回は Next.js の API Routes を使っていくので、/src/pages/api/newt/index.tsといった具合でファイルを作成しておきます。
作成できたら、その中で処理を行なっていきましょう!

import { NextApiRequest, NextApiResponse } from 'next'
import axios from 'axios'

// ここで各店舗のspaceUid,appUid,apiTokenを用意しておきます。
const ACCOUNTS = [
  {
    spaceUid: 'store1',
    appUid: 'contents-111111',
    apiToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  },
  {
    spaceUid: 'store2',
    appUid: 'contents-222222',
    apiToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  },
  ...
]

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const results = await Promise.all(
      // 各店舗のpostsモデルを取得していきます。
      ACCOUNTS.map(async ({ spaceUid, apiToken, appUid }) => {
        const { data } = await axios.get(
          `https://${spaceUid}.cdn.newt.so/v1/${appUid}/posts`,
          {
            headers: {
              // 認証のためにapiTokenが必要になるのでここで設定してあげましょう。
              Authorization: `Bearer ${apiToken}`,
            },
          }
        )
        return data
      })
    )
    res.status(200).json(results)
  } catch (error) {
    console.error(error)
    res.status(500).json({ message: 'Internal Server Error' })
  }
}

こんな感じで Newt から各店舗のデータを取得してくるのです。

そして、API Routes は設定できたので、フロント側でデータを扱うための処理を書いていきます!

今回は Next.js を使っているので/pages/index.tsgetStaticPropsでデータの取得を進めていきます。

export const getStaticProps = async () => {
  // API Routesで作成したデータを読み込んでいきます
  const res = await fetch('http://localhost:3000/api/newt')
  const data = await res.json()
  return {
    props: {
      data,
    },
  }
}

export default function Home({ data }) {
  console.log(data)
}

そうすると、こんな感じのデータを取得することができます。

[
  { skip: 0, limit: 100, total: 2, items: [ [Object], [Object] ] },
  { skip: 0, limit: 100, total: 1, items: [ [Object] ] }
]

Newt で作成したコンテンツは items の中に格納されているので、各店舗で作成したコンテンツの一覧情報を表示するためには items だけをまとめた配列にまとめる必要があります。

なので、以下でその処理をします。

const combinedItems = data.reduce((acc, obj) => {
  return acc.concat(obj.items)
}, [])

すると、先ほどの配列がこのようになりました。

[
  {
    _id: 'xxxxxxxxxxxxxxxxxx',
    _sys: {
      raw: [Object],
      customOrder: 2,
      createdAt: '2023-04-26T02:52:00.238Z',
      updatedAt: '2023-04-26T02:52:00.238Z'
    },
    title: 'テスト1',
    text: '<p>テスト1</p>'
  },
  {
    _id: 'xxxxxxxxxxxxxxxxxx',
    _sys: {
      raw: [Object],
      customOrder: 1,
      createdAt: '2023-04-26T02:47:48.601Z',
      updatedAt: '2023-04-26T02:47:48.601Z'
    },
    title: 'テスト2',
    text: '<p>テスト2</p>'
  },
  {
    _id: 'xxxxxxxxxxxxxxxxxx',
    _sys: {
      raw: [Object],
      customOrder: 1,
      createdAt: '2023-04-27T13:03:39.612Z',
      updatedAt: '2023-04-27T13:03:39.612Z'
    },
    title: 'テスト3',
    text: '<p>テスト3</p>'
  }
]

これらを展開していったら、それぞれ別々のスペースから作成したコンテンツでもまとめることができました!

参考

さいごに

今回は HeadlessCMS の Newt でのなんちゃってマルチテナンシー?的なことをしてみましたが、2023/6 ごろから料金プランの改定もあって、自分がやりたいことをするのには少しお金がかかってしまいそうだったので諦めました。

ですが、使い勝手は非常に素晴らしいものなので、仕様あっていてお金に余裕がある場合はぜひ使ってみてください!

また、今回の料金プランの改定で感じたのは、会社組織の運営する HeadlessCMS では料金面などの大幅な改定を受けてしまうと、首が回らなくなってしまうのでホスティングなど自分でやらないといけないことが多くて大変ですが、オープンソースの Strapi なども視野に入れてもいいかな?と思ってきました。

なので、これからは Strapi とかの記事が増えてくるかもしれませんが、今後ともよろしくお願いいたします。