Nuxt3でsequelize7を使う

javascript code NuxtJS
Photo by Markus Spiske on Pexels.com
この記事は約10分で読めます。

ORMであるsequelizeのバージョン7(執筆時点でalpha)では、デコレータを使ったモデル定義が可能です。本記事では、Nuxt3でsequelize7を使ってデータベースを検索してその結果を返すまでのステップを記述します。

テスト環境

  • mac 10.15.7
  • Node.js v20.13.0
  • nuxt@3.11.2
  • sequelize@6.37.3
  • @sequelize/core@7.0.0-alpha.41

セットアップ

Nuxt3とsequelizeをインストールします。今回データベースはSQLite3を使います。sequelizeライブラリは、sequelize-cliによるデータベース作成のためにインストールします。sequelize-cliはsequelize7にまだ対応していないためです。なお、sequelize7では、sequelizeは@sequelize/coreというパッケージ名に変更されています。

$ pnpm dlx nuxi@latest init nuxt-sequelize7
$ cd nuxt-sequelize7
# corepackの制限を一時的にoffにしたい場合
$ export COREPACK_ENABLE_STRICT=0
$ pnpm add @sequelize/core@alpha @sequelize/sqlite3 sequelize zod bcrypt
$ pnpm add -D sequelize-cli @types/brcypt

nuxt.config.ts

デコレータを使えるようにするため、以下の設定が必要です。TypeScriptのコンパイルオプションで、experimentalDecoratorsをtrueにします。また、tsconfig.jsonにも設定を加えます。VSCode上でのエラーも消えます。

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },

  nitro: {
    esbuild: {
      options: {
        tsconfigRaw: {
          // See https://github.com/nuxt/nuxt/issues/14126
          //     https://github.com/unjs/nitro/issues/273
          compilerOptions: {
            experimentalDecorators: true,
          },
        },
      },
    },
  },
});

tsconfig.json

{
  "extends": "../.nuxt/tsconfig.json",
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

モデル定義

sequelize-cliを使って環境を初期化します。databaseディレクトリ の下に関連ファイルを置くこととします。models/index.jsは使わないので削除しておきます。

$ ./node_modules/.bin/sequelize-cli  init --migrations-path database/migrations --models-path database/models --seeders-path database/seeders
$ tree config database
config
└── config.json
database
├── migrations
├── models
│   └── index.js
└── seeders

3 directories, 2 files
$ rm database/models/index.js

今回はSQLiteを使うのでconfig.jsonを修正します。

config/config.json

{
  "development": {
    "dialect": "sqlite",
    "storage": "./db_dev.sqlite"
  }
}

続いてモデルを作成していきます。今回は、2つのテーブルからなるシンプルな構成とします。

database/models/index.ts

ER図は以下のとおりです。

import {
  Sequelize,
  type HasManyCreateAssociationMixin,
  type InferAttributes,
  type InferCreationAttributes,
  type Options,
} from "@sequelize/core";
import config from "../../config/config.json";
import {
  DataTypes,
  Model,
  type CreationOptional,
  type NonAttribute,
} from "@sequelize/core";
import {
  Attribute,
  HasMany,
  NotNull,
  Table,
  CreatedAt,
  UpdatedAt,
  PrimaryKey,
  AutoIncrement,
  BeforeCreate,
  BeforeUpsert,
} from "@sequelize/core/decorators-legacy";
import type { SqliteDialect } from "@sequelize/sqlite3";
import bcrypt from "bcrypt";
const environment = process.env.NODE_ENV || "development";
const options = config[environment as keyof typeof config];
export const sequelize = new Sequelize({
  ...options,
} as Options<SqliteDialect>);

@Table({
  tableName: "Users",
  indexes: [{ fields: ["email"], unique: true }],
})
export class User extends Model<
  InferAttributes<User>,
  InferCreationAttributes<User>
> {
  @Attribute(DataTypes.INTEGER.UNSIGNED)
  @PrimaryKey
  @AutoIncrement
  declare readonly id: CreationOptional<number>;

  @Attribute(DataTypes.STRING)
  @NotNull
  declare name: string;

  @Attribute(DataTypes.STRING)
  @NotNull
  declare email: string;

  @Attribute(DataTypes.STRING)
  @NotNull
  declare password: string;

  @CreatedAt
  declare readonly createdAt: CreationOptional<Date>;

  @UpdatedAt
  declare readonly updatedAt: CreationOptional<Date>;

  @HasMany(() => Post, {
    foreignKey: "userId",
    inverse: {
      as: "author",
    },
  })
  declare posts?: NonAttribute<Post[]>;

  declare createPost: HasManyCreateAssociationMixin<Post, "userId">;

  @BeforeCreate
  static async hashPassword(instance: User) {
    const password = instance.getDataValue("password");
    if (password) {
      const hashedPassword = bcrypt.hashSync(password, 10);
      instance.setDataValue("password", hashedPassword);
    }
  }
}

@Table({
  tableName: "Posts",
})
export class Post extends Model<
  InferAttributes<Post>,
  InferCreationAttributes<Post>
> {
  @Attribute(DataTypes.INTEGER.UNSIGNED)
  @PrimaryKey
  @AutoIncrement
  declare readonly id: CreationOptional<number>;

  @Attribute(DataTypes.STRING)
  @NotNull
  declare title: string;

  @Attribute(DataTypes.STRING)
  @NotNull
  declare content: string;

  @Attribute({
    type: DataTypes.INTEGER.UNSIGNED,
    references: { table: "Users", key: "id" },
    onUpdate: "CASCADE",
    onDelete: "CASCADE",
  })
  declare userId: number;

  @CreatedAt
  declare readonly createdAt: CreationOptional<Date>;

  @UpdatedAt
  declare readonly updatedAt: CreationOptional<Date>;

  /** Declared by {@link User#posts} */
  declare author?: NonAttribute<User>;
}

sequelize.addModels([Post, User]);
await sequelize.sync();

// 簡単のためここでテストデータを作成
const user = await User.findOne({
  where: { email: "admin@example.com" },
});
if (!user) {
  await User.create({
    name: "admin",
    email: "admin@example.com",
    password: "password",
  });
}

APIの作成

記事投稿と参照のAPIを定義します。

server/api/posts.get.ts

import { Post } from "~/database/models";

export default defineEventHandler(async (event) => {
  const posts = await Post.findAll();
  return posts;
});

server/api/posts.post.ts

import { z } from "zod";
import { User } from "~/database/models";

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(
    event,
    z.object({
      title: z.string(),
      content: z.string(),
    }).parse,
  );

  const user = await User.findOne({ where: { name: "admin" } });
  const post = await user!.createPost(body);
  return post;
});

実行

準備ができましたので、アプリケーションを起動してみます。

$ pnpm run dev

VSCodeでデータベースを覗いてみましょう。VSCodeでSQLiteエクステンションをインストールすると、 VSCode内でデータベースを見ることができます。ちゃんとデータベースが作成され、ユーザーが作成されていることがわかります。

今回は画面を作成せず、devtoolsでAPIをコールしてみます。

まずは、記事を投稿してみます。良さそうですね。

続いて記事の参照。こちらも良さそうですね。

まとめ

Nuxt3でSequelize7を使用してみました。

Sequelize7ではモデル定義でデコレータを使用できます。なお、機能を使用するにはTypeScriptのexperimentalDecoratorsオプションを有効化する必要があります。また、nitroでも(内部でesbuildを使用)同様にオプションを有効化する必要があります。

参考リンク

サンプルプログラム

今回のサンプルプログラムは以下です。

moritetu/nuxt-sequelize7: nuxt3+sequelize7 sample (github.com)

コメント

タイトルとURLをコピーしました